根据类型键解析动态JSON

9
我正在寻找一种解决方案,它不需要引入额外的“通用”字段,如ValueData等,这将成为变体字段的占位符。
我有一个JSON规范,描述了几个大的结构体,其中大部分是简单值,但偶尔会有一个值本身就是一个结构体,其动态类型取决于某个字段的值。
例如,这两个JSON文档都应该解码为同一个Go结构体:
{ 
  "some_data": "foo",
  "dynamic_field": { "type": "A", "name": "Johnny" },
  "other_data": "bar"
}

并且

{
  "some_data": "foo",
  "dynamic_field": { "type": "B", "address": "Somewhere" },
  "other_data": "bar"
}

JSON结构已经确定,我无法更改。

Go结构必须像这样:

type BigStruct struct {
  SomeData     string    `json:"some_data"`
  DynamicField Something `json:"dynamic_field"`
  OtherData    string    `json:"other_data"`
}

问题是如何实际做到这一点以及那个Something类型应该是什么。
我首先将其制作为一个接口:
type Something interface {
  GetType() string
}

并且还有多个结构体和函数与之配合使用:

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

type DynamicTypeA struct {
  BaseDynamicType
  Name string `json:"name"`
}

type DynamicTypeB struct {
  BaseDynamicType
  Address string `json:"address"`
}

func (d *BaseDynamicType) GetType() string {
  return d.Type
}

原因在于,当我得到一个BigStruct实例时,我可以这样做:
switch big.DynamicField.GetType() {
  case "A": // do something with big.DynamicField cast to DynamicTypeA
  case "B": // do something with big.DynamicField cast to DynamicTypeB
}

然而,我陷入了困境 - 这种安排如何与UnmarshalJSON一起使用?我认为BigStruct应该实现UnmarshalJSON,它将在某种程度上检查dynamic_fieldType字段,然后根据此字段,使DynamicField成为DynamicTypeADynamicTypeB
但是如何做到呢?一种可能不起作用的方式是:
- 将DynamicField标记为json:"-" - 为BigStruct实现UnmarshalJSON - 在BigStructUnmarshalJSON中将JSON解组成map[string]interface{} - 检查地图中的dynamic_field值,手动构造DynamicTypeADynamicTypeB - 再次将相同数据解组到BigStruct中 - 使用手动创建的值修复DynamicField 但是,在第5步尝试将数据解组成BigStruct时,这将导致无限递归,因为它会调用当前正在执行的相同的UnmarshalJSON函数。
1个回答

17
type BigStruct struct {
    SomeData     string      `json:"some_data"`
    DynamicField DynamicType `json:"dynamic_field"`
    OtherData    string      `json:"other_data"`
}

type DynamicType struct {
    Value interface{}
}

func (d *DynamicType) UnmarshalJSON(data []byte) error {
    var typ struct {
        Type string `json:"type"`
    }
    if err := json.Unmarshal(data, &typ); err != nil {
        return err
    }
    switch typ.Type {
    case "A":
        d.Value = new(TypeA)
    case "B":
        d.Value = new(TypeB)
    }
    return json.Unmarshal(data, d.Value)

}

type TypeA struct {
    Name string `json:"name"`
}

type TypeB struct {
    Address string `json:"address"`
}

如果您不想或不能更改DynamicField的类型,可以将UnmarshalJSON方法放在BigStruct上,并声明一个临时类型以避免递归。

https://play.golang.com/p/oKMKQTdzp7s

func (b *BigStruct) UnmarshalJSON(data []byte) error {
    var typ struct {
        DF struct {
            Type string `json:"type"`
        } `json:"dynamic_field"`
    }
    if err := json.Unmarshal(data, &typ); err != nil {
        return err
    }

    switch typ.DF.Type {
    case "A":
        b.DynamicField = new(DynamicTypeA)
    case "B":
        b.DynamicField = new(DynamicTypeB)
    }

    type tmp BigStruct // avoids infinite recursion
    return json.Unmarshal(data, (*tmp)(b))
}

https://play.golang.com/p/at5Okp3VU2u


谢谢,我也发现了那个变体,但它需要修改结构来创建“Value”字段,而原始结构中没有这个字段,所以我不能使用这种方法。 - Ivan Voras
@IvanVoras 然后像你在问题中概述的那样,在 BigStruct 上放置 UnmarshalJSON,并为避免递归问题,你可以在 UnmarshalJSON 中声明一个新的临时类型,然后将其转换为 BigStruct。 - mkopriva
2
@IvanVoras 我已经扩展了答案,添加了一个更符合您要求的解决方案。 - mkopriva
1
@rickydj 是 json.Unmarshal 调用了该方法。文档中说道:*"要将 JSON 反序列化为实现 Unmarshaler 接口的值,Unmarshal会调用该值的 UnmarshalJSON 方法,即使输入是 JSON null 也是如此。"* - mkopriva
我在这里看到的问题是,go语言(包括编辑器智能感知)无法确定DynamicField实际上是否具有名称或地址值。因此,你需要额外的努力来强制或告诉go语言,确实,_这个_是DynamicTypeA,而_那个_是DynamicTypeB,例如:https://play.golang.com/p/7ck36wnmPiz - fullStackChris
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接