【问题标题】:Is there a way to have json.Unmarshal() select struct type based on "type" property?有没有办法让 json.Unmarshal() 根据“类型”属性选择结构类型?
【发布时间】:2017-07-31 23:54:01
【问题描述】:

我有一些 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 用汽车和飞机填充我的车辆部分;相反(不出所料)我只是得到“无法将对象解组为 main.vehicle 类型的 Go 值”。

作为参考,以下是所涉及类型的合适定义:

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 umarhsaller 根据正在解码的数据的某些部分内容(在本例中为类型字段)选择要使用的类型?

(请注意,这不是Unmarshal JSON with unknown fields 的重复,因为我希望切片中的每个项目都具有不同的动态类型,并且从我知道的'type'属性的值确切地期望什么字段——我只是不知道如何告诉 json.Unmarshal 如何将 'type' 属性值映射到 Go 类型。)

【问题讨论】:

  • 是的,你可以实现json.Unamrshaler,但这取决于你希望你的最终数据结构是什么样子。你对vehicle 的期望究竟是什么?
  • 如问题所示,我希望我的最终数据结构v 的类型为[]vehicle(或[]*vehicle,也可以),其中v[0] 是@ 987654338@ 和v[1] 是一架飞机。
  • @JimB:你能不重复这个问题吗?我相信我已经充分澄清以证明它不是Unmarshal JSON with unknown fields 的“完全重复”。
  • 我看不出这与其他多次被问到的情况有何不同。基本答案始终是对链接副本的建议的组合。您可以解组为通用结构并手动复制字段,或者您可以只读取“类型”字段并将对象再次解组为特定类型。是实现 json.Unmarshaler,还是实现从 []json.RawMessage{} 转换为 []vehicle 的函数取决于您,但这将是一个多步骤的过程,您需要实现。
  • 嗯,一个问题是我无法在车辆上定义 UnmarshalJSON 方法,因为它是一个接口。我可以使用单个匿名车辆字段创建包装器结构并在其中定义 UnmarhsalJSON(这可行),但最终我得到了一个 []vehicleWrapper 而不是 []vehicle,这不是我想要的。

标签: json go interface


【解决方案1】:

从类似的问题中得到答案:Unmarshal JSON with unknown fields,我们可以构造一些方法来在 []vehicle 数据结构中取消对这个 JSON 对象的映射。

“Unmarshal with Manual handling”版本可以通过使用通用的[]map[string]interface{} 数据结构来完成,然后从地图切片构建正确的vehicles。为简洁起见,此示例确实省略了 json 包会执行的对丢失或键入错误字段的错误检查。

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

我们可以再次利用 json 包来处理各个结构的解组和类型检查,方法是通过第二次直接解组为正确的类型。这可以通过在 []vehicle 类型上定义一个 UnmarshalJSON 方法首先将 JSON 对象拆分为原始消息来封装到 json.Unmarshaler 实现中。

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】:

    两遍方法效果很好,但也有mapstructure 包的选项,它就是为了做到这一点而创建的。

    【讨论】:

      【解决方案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

      【讨论】:

      【解决方案4】:

      如果属性是字符串,您可以使用 .(string) 来转换属性,因为来源是接口。
      您可以通过以下方式使用它:
      v["type"].(string)

      【讨论】:

      • 您回答的问题似乎与我提出的不同。我不是在努力访问 type 字段,而是让json.Unmarshal 智能地使用它。
      猜你喜欢
      • 2019-09-24
      • 1970-01-01
      • 2018-07-21
      • 2013-04-21
      • 2023-03-20
      • 2016-10-18
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多