tekumara
tekumara

Reputation: 8807

Deserialize into map[string]interface{} as a concrete map type

type Foo struct {
    M map[string]interface{} `json:"m"`
}

type Bar struct {
    I int `json:"i"`
}

type Bar2 struct {
    S string `json:"s"`
}


func do() {
    concreteFoo1 := Foo {
        M: make(map[string]Bar),
    }
    json.Unmarshal([]byte(`{"m": {"a": { "i": 1 }}}`), &concreteFoo1)

    concreteFoo2 := Foo {
        M: make(map[string]Bar2),
    }

    json.Unmarshal([]byte(`{"m": {"a": { "s": "hello" }}}`), &concreteFoo2)

}

fails to compile with:

cannot use make(map[string]Bar) (type map[string]Bar) as type map[string]interface {} in field value

cannot use make(map[string]Bar2) (type map[string]Bar2) as type map[string]interface {} in field value

How can I get this to compile, and support both variants of Foo?

Upvotes: 2

Views: 1579

Answers (5)

Eugene Lisitsky
Eugene Lisitsky

Reputation: 12845

I've looked again and think custom unmarshaler can be good option.

func (f *Foo) UnmarshalJSON(b []byte) error {
    var tmp struct {
        M map[string]json.RawMessage `json:"m"`
    }
    var foo = Foo{
        M: make(map[string]interface{}),
    }
    var err error
    //for key, rawValue := range
    err = json.Unmarshal(b, &tmp)
    if err != nil {
        return err
    }
    for key, rawValue := range tmp.M {
        var bar Bar
        if json.Unmarshal(rawValue, &bar) == nil && bar.I != 0 { // custom check
            foo.M[key] = bar
            continue
        }
        var bar2 Bar2
        if json.Unmarshal(rawValue, &bar2) == nil && bar2.S != "" { // custom check
            foo.M[key] = bar2
            continue
        }
    }
    *f = foo
    return nil
}

Advantages:

  • Logics in one place

  • Standard call UnmarshalJSON

  • Can handle several subobjects

https://play.golang.org/p/ug9rewqjWw

Upvotes: 0

mkopriva
mkopriva

Reputation: 38203

If the set of possible Bar types is infinite you should use the solution from Stud's answer. After unmarshaling you just need to do one type assertion to get the map, which is much better then doing type assertion on each value of a map[string]interface{} type.

If, however, the set is limited to only a few types you could create a parent Bar type that embeds the list of all the possible Bar variations. In case there's a possibility that the fields of the embedded types would collide you can still have the embedding type implement the json.Unmarshaler interface and do some custom logic on what to unmarshal where.

E.g. something like this:

type BarSet struct {
    *Bar
    *Bar2
}

type Bar struct {
    I int `json:"i"`
}

type Bar2 struct {
    S string `json:"s"`
}

https://play.golang.org/p/tJfqtnP-CX

And if you want to be as strict as possible you should create separate Foo types, each with their distincitve M map[string]... field.

Upvotes: 1

Eugene Lisitsky
Eugene Lisitsky

Reputation: 12845

You can make generalized interface

type Foo struct {
    M map[string]Ibar `json:"m"`
}

type Ibar interface{}

And unpack into it.

When you need specific value of variable make type assertion.

https://play.golang.org/p/fcs-Yp-ck2

Upvotes: 0

Stud
Stud

Reputation: 11

Change this line: M map[string]interface{} json:"m"

into : M interface{} json:"m"

https://play.golang.org/p/qRxo81amA7

Upvotes: 1

laz
laz

Reputation: 28638

Change Foo.m to be map[string]Bar instead of what you have, that will allow it to compile. To make it work, you need to change Foo.m to Foo.M and Bar.i to Bar.I. The Go JSON library will not unmarshal to or marshal from unexported properties. Mapping the uppercase properties to lowercase JSON elements requires using tags. Full working example here.

Upvotes: 1

Related Questions