Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is there a way to have json.Unmarshal() select struct type based on "type" property?

Tags:

json

interface

go

I have some JSON of the form:

[{
    "type": "car",
    "color": "red",
    "hp": 85,
    "doors": 4
}, {
    "type": "plane",
    "color": "blue",
    "engines": 3
}]

I have types car and plane that satisfy a vehicle interface; I'd like to be able to write:

var v []vehicle
e := json.Unmarshal(myJSON, &v)

... and have JSON fill my slice of vehicles with a car and a plane; instead (and unsurprisingly) I just get "cannot unmarshal object into Go value of type main.vehicle".

For reference, here are suitable definitions of the types involved:

type vehicle interface {
    vehicle()
}

type car struct {
    Type  string
    Color string
    HP    int
    Doors int
}

func (car) vehicle() { return }

type plane struct {
    Type    string
    Color   string
    Engines int
}

func (plane) vehicle() { return }

var _ vehicle = (*car)(nil)
var _ vehicle = (*plane)(nil)

(Note that I'm actually totally uninterested in the t field on car and plane - it could be omitted because this information will, if someone successfully answers this question, be implicit in the dynamic type of the objects in v.)

Is there a way to have the JSON umarhsaller choose which type to use based on some part of the contents (in this case, the type field) of the data being decoded?

(Note that this is not a duplicate of Unmarshal JSON with unknown fields because I want each item in the slice to have a different dynamic type, and from the value of the 'type' property I know exactly what fields to expect—I just don't know how to tell json.Unmarshal how to map 'type' property values onto Go types.)

like image 362
cpcallen Avatar asked Mar 10 '17 15:03

cpcallen


People also ask

How do you Unmarshal a JSON?

To unmarshal JSON into a pointer, Unmarshal first handles the case of the JSON being the JSON literal null. In that case, Unmarshal sets the pointer to nil. Otherwise, Unmarshal unmarshals the JSON into the value pointed at by the pointer. If the pointer is nil, Unmarshal allocates a new value for it to point to.

How does JSON Unmarshal work in Golang?

Marshal function, will take JSON data and translate it back into Go data. You provide json. Unmarshal with the JSON data as well as the Go variable to put the unmarshalled data into and it will either return an error value if it's unable to do it, or a nil error value if it succeeded.

What is JSON Marshal Unmarshal?

Marshaling/Unmarshaling refers to the process of mapping JSON types from and to Go data types and primitives.


4 Answers

Taking the answers from the similar question: Unmarshal JSON with unknown fields, we can construct a few ways to unamrshal this JSON object in a []vehicle data structure.

The "Unmarshal with Manual Handling" version can be done by using a generic []map[string]interface{} data structure, then building the correct vehicles from the slice of maps. For brevity, this example does leave out the error checking for missing or incorrectly typed fields which the json package would have done.

https://play.golang.org/p/fAY9JwVp-4

func NewVehicle(m map[string]interface{}) vehicle {
    switch m["type"].(string) {
    case "car":
        return NewCar(m)
    case "plane":
        return NewPlane(m)
    }
    return nil
}

func NewCar(m map[string]interface{}) *car {
    return &car{
        Type:  m["type"].(string),
        Color: m["color"].(string),
        HP:    int(m["hp"].(float64)),
        Doors: int(m["doors"].(float64)),
    }
}

func NewPlane(m map[string]interface{}) *plane {
    return &plane{
        Type:    m["type"].(string),
        Color:   m["color"].(string),
        Engines: int(m["engines"].(float64)),
    }
}

func main() {
    var vehicles []vehicle

    objs := []map[string]interface{}{}
    err := json.Unmarshal(js, &objs)
    if err != nil {
        log.Fatal(err)
    }

    for _, obj := range objs {
        vehicles = append(vehicles, NewVehicle(obj))
    }

    fmt.Printf("%#v\n", vehicles)
}

We could leverage the json package again to take care of the unmarshaling and type checking of the individual structs by unmarshaling a second time directly into the correct type. This could all be wrapped up into a json.Unmarshaler implementation by defining an UnmarshalJSON method on the []vehicle type to first split up the JSON objects into raw messages.

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

type Vehicles []vehicle


func (v *Vehicles) UnmarshalJSON(data []byte) error {
    // this just splits up the JSON array into the raw JSON for each object
    var raw []json.RawMessage
    err := json.Unmarshal(data, &raw)
    if err != nil {
        return err
    }

    for _, r := range raw {
        // unamrshal into a map to check the "type" field
        var obj map[string]interface{}
        err := json.Unmarshal(r, &obj)
        if err != nil {
            return err
        }

        vehicleType := ""
        if t, ok := obj["type"].(string); ok {
            vehicleType = t
        }

        // unmarshal again into the correct type
        var actual vehicle
        switch vehicleType {
        case "car":
            actual = &car{}
        case "plane":
            actual = &plane{}
        }

        err = json.Unmarshal(r, actual)
        if err != nil {
            return err
        }
        *v = append(*v, actual)

    }
    return nil
}
like image 128
JimB Avatar answered Nov 14 '22 08:11

JimB


The two passes approach works fine, but there is also the option of the mapstructure package, that was created to do exactly this.

like image 42
caiman Avatar answered Nov 14 '22 08:11

caiman


JSON decoding and encoding in Go is actually surprisingly well at recognizing fields inside embedded structs. E.g. decoding or encoding the following structure works when there is no overlapping fields between type A and type B:

type T struct{
    Type string `json:"type"`
    *A
    *B
}

type A struct{
    Baz int `json:"baz"`
}

type B struct{
    Bar int `json:"bar"`
}

Be aware that if both "baz" and "bar" are set in the JSON for the example above, both the T.A and T.B properties will be set.

If there is overlapping fields between A and B, or just to be able to better discard invalid combinations of fields and type, you need to implement the json.Unmarshaler interface. To not have to first decode fields into a map, you can extend the trick of using embedded structs.

type TypeSwitch struct {
    Type string `json:"type"`
}

type T struct {
    TypeSwitch
    *A
    *B
}

func (t *T) UnmarshalJSON(data []byte) error {
    if err := json.Unmarshal(data, &t.TypeSwitch); err != nil {
        return err
    }
    switch t.Type {
    case "a":
        t.A = &A{}
        return json.Unmarshal(data, t.A)
    case "b":
        t.B = &B{}
        return json.Unmarshal(data, t.B)
    default:
        return fmt.Errorf("unrecognized type value %q", t.Type)
    }

}

type A struct {
    Foo string `json:"bar"`
    Baz int    `json:"baz"`
}

type B struct {
    Foo string `json:"foo"`
    Bar int    `json:"bar"`
}

For marshaling back, json.Marshaler must also be implemented if there is overlapping fields.

Full example: https://play.golang.org/p/UHAdxlVdFQQ

like image 26
Sindre Myren Avatar answered Nov 14 '22 10:11

Sindre Myren


I was facing the same problem.

I'm using the lib github.com/mitchellh/mapstructure together the encoding/json.

I first, unmarshal the json to a map, and use mapstructure to convert the map to my struct, e.g.:

type (
  Foo struct {
    Foo string `json:"foo"`
  }
  Bar struct {
    Bar string `json:"bar"`
  }
)

func Load(jsonStr string, makeInstance func(typ string) any) (any, error) {
    // json to map
    m := make(map[string]any)
    e := json.Unmarshal([]byte(jsonStr), &m)
    if e != nil {
        return nil, e
    }

    data := makeInstance(m["type"].(string))

    // decoder to copy map values to my struct using json tags
    cfg := &mapstructure.DecoderConfig{
        Metadata: nil,
        Result:   &data,
        TagName:  "json",
        Squash:   true,
    }
    decoder, e := mapstructure.NewDecoder(cfg)
    if e != nil {
        return nil, e
    }
    // copy map to struct
    e = decoder.Decode(m)
    return data, e
}

Using:

f, _ := Load(`{"type": "Foo", "foo": "bar"}`, func(typ string) any {
        switch typ {
        case "Foo":
            return &Foo{}
        }
        return nil
    })
like image 1
Beto Neto Avatar answered Nov 14 '22 08:11

Beto Neto