【问题标题】:How to unmarshal a JSON field which is either an struct or array of stuct?如何解组一个结构体或结构体数组的 JSON 字段?
【发布时间】:2021-10-13 09:25:09
【问题描述】:

用例

我有一个 API,我在其中接收作为对象或对象数组的用户输入。像这样:

没有数组的 JSON

{
  "sign": {
    "page_no": 1,
    "x_coord": 100,
    "y_coord": 300
  }
}

带有数组的 JSON

{
  "sign": [
    {
      "page_no": 1,
      "x_coord": 100,
      "y_coord": 300
    },
    {
      "page_no": 2,
      "x_coord": 200,
      "y_coord": 400
    }
  ]
}

将结构解组为

type Document struct {
  Sign []Sign `json:"sign"` // or just Sign
}

type Sign struct {
  PageNo int `json:"page_no"`
  XCoord int `json:"x_coord"`
  YCoord int `json:"y_coord"`
}

由于遗留原因,我无法将Document 上的Sign 字段作为所有用户输入的数组,因此它需要既是Sign 的数组,又是Sign 的数组。

如何解组它以便它可以处理下面的两个 JSON 请求?

我知道...

我们可以使用map[string]interface{} 代替结构体,但这会导致在我使用结构体时您不必执行的太多关键断言,因为我可以利用它们的零值。

另外,this answer on Stackoverflow 似乎还可以,但我想知道有没有更好的方法?

【问题讨论】:

  • 如果 JSON 是 Sign 类型,您是否可以引入依赖或错误,并解码两次?
  • 链接的答案是如何做到这一点(实现UnmarshalJSON()*Sign)。遗留设计很糟糕,这就是后果。
  • 相关视频在这里:youtu.be/vsN11YAEJHY?t=604

标签: arrays json go unmarshalling


【解决方案1】:

通过实现自定义解组器,您完全可以在没有 interface{}(或类型断言)的情况下做到这一点。我将通过查看 JSON 内部来实现解组器,并检查它是否以方括号或花括号开头...跳过空格。

请参阅文档中的json.Unmarshaler 接口。

type Document struct {
    Sign SignList
}

type SignList []Sign

func (l *SignList) UnmarshalJSON(d []byte) error {
    for _, b := range d {
        switch b {
        // These are the only valid whitespace in a JSON object.
        case ' ', '\n', '\r', '\t':
        case '[':
            return json.Unmarshal(d, (*[]Sign)(l))
        case '{':
            var obj Sign
            if err := json.Unmarshal(d, &obj); err != nil {
                return err
            }
            *l = []Sign{obj}
            return nil
        default:
            return errors.New("sign must be object or list")
        }
    }
    return errors.New("sign must be object or list")
}

这可能看起来有点笨拙。那好吧。除了 encoding/json 之外,还有其他用于 Go 的 JSON 解组库。如果你愿意,可以尝试一个替代的 JSON 库。此代码仅适用于 encoding/json。

您可以通过以下方式测试此代码是否有效:

package main

import (
    "fmt"
    "encoding/json"
    "errors"
)

type Sign struct {
    X int
}

func main() {
    cases := []string {
        `{"Sign": {"X": 1}}`,
        `{"Sign": [{"X": 1}, {"X": 2}]}`,
    }
    for _, c := range cases {
        var doc Document
        if err := json.Unmarshal([]byte(c), &doc); err != nil {
            fmt.Println("Error:", err)
            continue
        }
        fmt.Printf("Document: %+v\n", &doc)
    }
}

打印出来:

Document: &{Sign:[{X:1}]}
Document: &{Sign:[{X:1} {X:2}]}

【讨论】:

  • 你推荐的其他库可以抽象出这种笨拙吗?
  • @Dietrich 我似乎在这里遗漏了一些非常明显的东西,但我知道你在这里偷看 JSON。您在哪里确保只检查 sign 键的类型,以便将来添加新的顶级属性不会破坏这一点?
  • @AyushGupta:那是不必要的。如果您想了解json.Unmarshaler 接口的工作原理,请参阅文档。 pkg.go.dev/encoding/json#Unmarshaler
  • @Aziz:我没有任何特定的库可以推荐。如果您决定不喜欢使用 encoding/json,您可能需要阅读这些库的文档。
【解决方案2】:

使用自定义 json unmarshaler 方法创建切片类型。然后在那个解组器中,您可以检测您拥有哪种类型的输入,并采取相应的行为。这种方法在我的视频Advanced JSON handling in Go中有更详细的介绍。

type Signs []Sign

func (s *Signs) UnmarshalJSON(d []byte) error {
    if d[0] == '{' {
        // We know it's a single object
        var v Sign
        err := json.Unmarshal(d, &v)
        *s = Signs{v}
        return err
    }
    // Otherwise it's an array
    var v []Sign
    err := json.Unmarshal(d, &v)
    *s = Signs(v)
    return err
}

【讨论】:

    【解决方案3】:

    实现一个类型来测试,它可以验证您的数据是属于的,并将其用作检查。

    例如:

    package main
    
    type SignTest struct {
        Sign map[string]interface{} `json:"sign"`
    }
    
    func main() {
        s := `{
      "sign": [
        {
          "page_no": 1,
          "x_coord": 100,
          "y_coord": 300
        },
        {
          "page_no": 2,
          "x_coord": 200,
          "y_coord": 400
        }
      ]
    }`
    
        err := json.Unmarshal([]byte(s), &SignTest{})
    
        if err != nil {
            fmt.Println("Document Type")
        } else {
            fmt.Println("Sign Type")
        }
    }
    

    打印Document Type,同时

    package main
    
    
    type SignTest struct {
        Sign map[string]interface{} `json:"sign"`
    }
    
    func main() {
        s := `{
      "sign": {
        "page_no": 1,
        "x_coord": 100,
        "y_coord": 300
      }
    }`
    
        err := json.Unmarshal([]byte(s), &SignTest{})
    
        if err != nil {
            fmt.Println("Document Type")
        } else {
            fmt.Println("Sign Type")
        }
    }
    

    打印Sign Type

    但是在解组除符号以外的对象是否为数组时,可能会出现错误。如果您想要更安全,请在将其假设为 @987654325 之前检查错误的错误消息,该错误应该与预期一致@。

    【讨论】:

    • 解组除符号以外的对象是否为数组时可能会出错。
    • @Aziz 是的,这就是为什么我说If you want even more safety, check the error message of the error before parsing it as Document
    • 您只需要检查错误消息是否符合预期,当它应该是一个文档时,现在是:json: cannot unmarshal array into Go struct field SignTest.sign of type map[string]interface {} 按照当前代码
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2016-05-04
    • 1970-01-01
    • 1970-01-01
    • 2021-06-05
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多