有没有办法让json.Unmarshal()根据“ type”属性选择结构类型?

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.)

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
", 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
}