如何解组一个既可以是结构体又可以是结构体数组的 JSON 字段?

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

How to unmarshal a JSON field which is either an struct or array of stuct?

问题

使用案例

我有一个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"` // 或者只使用 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{} 而不是结构体,但这会导致太多的键断言,而当我使用结构体时,你不必这样做,因为我可以利用它们的零值。

此外,Stackoverflow上的这个答案看起来还可以,但我想知道是否有更好的方法?

英文:

Use case

I have an API in which I receive the user input as an object or array of objects. Like this:

JSON without Array

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

JSON with Array

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

Struct to Unmarshal to

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

I cannot make the Sign field on the Document as an array for all user inputs due to legacy reasons so It needs to be both array of Sign as well as just Sign.

How to unmarshal it so that it can handle both of the JSON requests below?

I Know That...

We can use map[string]interface{} instead of the struct but that will lead to too many key assertions which you don't have to do when I am using struct because I can leverage the zero values for them.

Also, this answer on Stackoverflow seems okay but I wanted to know is a there better way to do it?

答案1

得分: 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 {
        // 这些是JSON对象中唯一有效的空白字符。
		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}]}
英文:

You can do this without interface{} (or type assertions) at all, by implementing a custom unmarshaler. I would implement the unmarshaler by peeking inside the JSON, and checking to see if it starts with a square bracket or curly brace... skipping whitespace.

See the json.Unmarshaler interface in the docs.

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

This may seem a little bit clumsy. Oh well. There are alternative JSON unmarshaling libraries for Go besides encoding/json. Try out an alternative JSON library if you like. This code works with just encoding/json.

Here is how you can test that this code works:

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

This prints:

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

答案2

得分: 1

创建一个具有自定义JSON解组方法的切片类型。然后在该解组方法中,你可以检测输入的类型,并相应地进行处理。这种方法在我的视频《Go中的高级JSON处理》中有详细介绍(链接:https://youtu.be/vsN11YAEJHY?t=604)。

type Signs []Sign

func (s *Signs) UnmarshalJSON(d []byte) error {
    if d[0] == '{' {
        // 我们知道它是一个单个对象
        var v Sign
        err := json.Unmarshal(d, &v)
        *s = Signs{v}
        return err
    }
    // 否则它是一个数组
    var v []Sign
    err := json.Unmarshal(d, &v)
    *s = Signs(v)
    return err
}
英文:

Create a slice type with a custom json unmarshaler method. Then in that unmarshaler, you can detect which type of input you have, and behave accordingly. This approach is detailed further in my video 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

得分: -1

实现一个类型来测试并验证数据是否符合要求,并使用该类型进行检查。

例如:

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

但是,在解组对象时,除了判断 sign 是否为数组之外,可能会出现其他错误。如果你想要更安全,可以在假设为 Document 之前检查错误消息是否符合预期。

英文:

Implement a type to test which can validate your data is of, and use that to as a check.

For example:

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

Prints Document Type, while

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

Prints Sign Type.

But here might be an error while unmarshalling the object other than the sign is an array or not., If you want even more safety, check the error message of the error that is should be as expected before assuming it as Document.

huangapple
  • 本文由 发表于 2021年8月10日 01:27:01
  • 转载请务必保留本文链接:https://go.coder-hub.com/68716216.html
匿名

发表评论

匿名网友

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

确定