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

huangapple go评论81阅读模式
英文:

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

问题

我有一些形式为JSON的数据:

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

我有满足车辆接口的carplane类型;我想要能够编写:

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

... 并且希望JSON将我的车辆切片填充为一辆车和一架飞机;但是(并且不出所料),我只得到了"cannot unmarshal object into Go value of type main.vehicle"的错误。

以下是涉及的类型的适当定义:

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)

(请注意,我对carplane上的t字段实际上完全不感兴趣 - 它可以被省略,因为如果有人成功回答这个问题,这个信息将在v中的对象的动态类型中是隐含的。)

是否有一种方法可以让JSON解码器根据数据的某个部分(在这种情况下是类型字段)选择要使用的类型?

(请注意,这不是Unmarshal JSON with unknown fields的重复问题,因为我希望切片中的每个项具有不同的动态类型,并且根据"type"属性的值,我确切地知道要期望哪些字段,只是不知道如何告诉json.Unmarshal如何将"type"属性值映射到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.)

答案1

得分: 13

根据类似问题https://stackoverflow.com/questions/33436730/unmarshal-json-with-unknown-fields的答案,我们可以构建几种将此JSON对象解组为[]vehicle数据结构的方法。

"手动处理解组"版本可以通过使用通用的[]map[string]interface{}数据结构,然后从映射切片构建正确的vehicles来完成。为简洁起见,此示例省略了缺失或类型不正确的字段的错误检查,这是json包会执行的操作。

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

我们可以再次利用json包来处理单个结构体的解组和类型检查,通过直接第二次解组为正确的类型,这可以通过在[]vehicle类型上定义一个UnmarshalJSON方法来封装为json.Unmarshaler实现。

type Vehicles []vehicle

func (v *Vehicles) UnmarshalJSON(data []byte) error {
    // 这只是将JSON数组拆分为每个对象的原始JSON
    var raw []json.RawMessage
    err := json.Unmarshal(data, &raw)
    if err != nil {
        return err
    }

    for _, r := range raw {
        // 解组为映射以检查"type"字段
        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
        }

        // 再次解组为正确的类型
        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
}
英文:

Taking the answers from the similar question: https://stackoverflow.com/questions/33436730/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
}

答案2

得分: 3

在Go语言中,JSON的解码和编码实际上非常擅长识别嵌套结构中的字段。例如,在类型A和类型B之间没有重叠字段时,解码或编码以下结构是有效的:

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

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

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

请注意,如果JSON中同时设置了"baz"和"bar",那么T.AT.B属性都将被设置。

如果AB之间存在重叠字段,或者为了更好地丢弃无效的字段和类型组合,您需要实现json.Unmarshaler接口。为了不必先将字段解码为映射,您可以扩展使用嵌套结构的技巧。

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

如果存在重叠字段,则还必须实现json.Marshaler接口以进行编组。

完整示例:https://play.golang.org/p/UHAdxlVdFQQ

英文:

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

答案3

得分: 1

两次遍历的方法很好,但也有一个选项是使用mapstructure包,该包专门用于执行这个任务。

英文:

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

答案4

得分: 1

我遇到了同样的问题。

我正在使用github.com/mitchellh/mapstructure库和encoding/json库。

首先,我将JSON解组为一个映射,然后使用mapstructure将映射转换为我的结构体,例如:

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

func Load(jsonStr string, makeInstance func(typ string) interface{}) (interface{}, error) {
	// JSON转为映射
	m := make(map[string]interface{})
	e := json.Unmarshal([]byte(jsonStr), &m)
	if e != nil {
		return nil, e
	}

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

	// 解码器将映射值复制到我的结构体中,使用JSON标签
	cfg := &mapstructure.DecoderConfig{
		Metadata: nil,
		Result:   &data,
		TagName:  "json",
		Squash:   true,
	}
	decoder, e := mapstructure.NewDecoder(cfg)
	if e != nil {
		return nil, e
	}
	// 将映射复制到结构体中
	e = decoder.Decode(m)
	return data, e
}

使用方法:

f, _ := Load(`{"type": "Foo", "foo": "bar"}`, func(typ string) interface{} {
		switch typ {
		case "Foo":
			return &Foo{}
		}
		return nil
	})
英文:

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

答案5

得分: -2

如果属性是一个字符串,你可以使用.(string)来将属性转换为字符串,因为原始类型是一个接口。
你可以按照以下方式使用它:
v["type"].(string)

英文:

If the property is a string you can use .(string) for casting the property because the origin is an interface.<br>
You can use it the next way:<br>
v[&quot;type&quot;].(string)

huangapple
  • 本文由 发表于 2017年3月10日 23:15:19
  • 转载请务必保留本文链接:https://go.coder-hub.com/42721732.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定