Golang/JSON | DisallowUnknownFields 和自定义结构的序列化方法

在 Go 中,对于自定义结构的序列化和反序列化存在几个问题。

Q1:如何保证待反序列化的字符串只包含所定义的结构中的字段?

Go 1.10 起,标准库 encoding/json 提供了方法 func (*Decoder) DisallowUnknownFields。调用该方法表示,当目标是一个结构,并且输入流中包含任何不匹配该结构的非忽略的导出字段时,Decoder 会返回一个错误。

举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import (
"encoding/json"
"fmt"
"log"
"strings"
)

func main() {
var jsonStream = `{"Name": "Ed", "Text": "Knock knock."}`
type Message struct {
Name string
}
dec := json.NewDecoder(strings.NewReader(jsonStream))
dec.DisallowUnknownFields()
var m Message
err := dec.Decode(&m)
if err != nil {
log.Fatal(err)
}
fmt.Printf("%s\n", m.Name)
}

在上面的例子中,jsonStream 定义了一个结构 Message 不存在的字段 "Text"。接下来,声明一个 Decoder,并且调用 DisallowUnknownFields 方法。

运行会发现,反序列化失败:

1
2009/11/10 23:00:00 json: unknown field "Text"

如果结构 Message 中有一个忽略的导出 Text 字段,又会发生什么呢?稍微改动下上面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func main() {
var jsonStream = `{"Name": "Ed", "Text": "Knock knock."}`
type Message struct {
Name string
Text string `json:"-"` // 增加一个字段定义
}
dec := json.NewDecoder(strings.NewReader(jsonStream))
dec.DisallowUnknownFields()
var m Message
err := dec.Decode(&m)
if err != nil {
log.Fatal(err)
}
fmt.Printf("%s\n", m.Name)
}

运行,得到同样的错误:

1
2009/11/10 23:00:00 json: unknown field "Text"

同理,如果 Text 字段变成未导出字段,也会出现相同的报错:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func main() {
var jsonStream = `{"Name": "Ed", "Text": "Knock knock."}`
type Message struct {
Name string
text string
}
dec := json.NewDecoder(strings.NewReader(jsonStream))
dec.DisallowUnknownFields()
var m Message
err := dec.Decode(&m)
if err != nil {
log.Fatal(err)
}
fmt.Printf("%s\n", m.Name)
}

运行 验证一下吧。

Q2:如何判断某个字段是否存在?

Go 结构的零值导致了我们无法通过判断字段值是否等于某个值来确定 JSON 字符串中是否存在某个字段。

此外,如果将字段类型定义为指针的话,则无法区分该字段的值就是 null 的场景。

当然,我们并非无计可施。encoding/json 允许我们为自定义结构定义序列化和反序列化方法。只要分别实现 MarshalJSON() ([]byte, error)UnmarshalJSON([]byte) error 方法即可。

来看下如何通过 MarshalJSON 方法来判断某个字段是否存在:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
type Int struct {
Valid bool // 表示是否为有效值
Set bool // 表示是否设置
Value int
}

// 自定义反序列化方法
func (i *Int) UnmarshalJSON(data []byte) error {
// 如果调用了该方法,说明设置了该值
i.Set = true
if string(data) == "null" {
// 表明该字段的值为 null
return nil
}

var temp int
if err := json.Unmarshal(data, &temp); err != nil {
return err
}
i.Value = temp
i.Valid = true
return nil
}

// 自定义序列化方法
func (i Int) MarshalJSON() ([]byte, error) {
return []byte(strconv.Itoa(i.Value)), nil
}

在上面的例子中,我们定义了一个结构体 Int 来替代基本结构 int。并且定义了两个字段来表示是否设置及是否有效。这样,在序列化之后,我们就可以通过这两个字段来检查了。

此外,因为不希望序列化后出现这两个布尔值,因此还需要自定义序列化方法 MarshalJSON

下面,简单测试一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
type A struct{ Val Int }

func do(bytes []byte) (A, error) {
var a struct{ Val Int }
err := json.Unmarshal(bytes, &a)
return a, err
}

func main() {
notSet := []byte(`{}`)
setNull := []byte(`{"val": null}`)
setValid := []byte(`{"val": 123}`)
setWrongType := []byte(`{"val": "123"}`)

a, err := do(notSet)
log.Printf("NotSet|set:%t|valid:%t|err: %v\n", a.Val.Set, a.Val.Valid, err)

a, err = do(setNull)
log.Printf("SetNull|set:%t|valid:%t|err: %v\n", a.Val.Set, a.Val.Valid, err)

a, err = do(setValid)
log.Printf("SetValid|set:%t|valid:%t|err: %v\n", a.Val.Set, a.Val.Valid, err)

a, err = do(setWrongType)
log.Printf("SetWrongType|set:%t|valid:%t|err: %v\n", a.Val.Set, a.Val.Valid, err)
}

可以运行一下查看结果。

Q3:如何让 omitempty 选项对自定义结构体生效?

如果对字段使用了 omitempty 选项,那么在序列化过程中,如果该字段具有零值(即 false、0、nil 指针、nil 接口值和任何空数组、空 slice、空 map 或者空字符串),那么会忽略该字段。

但是,当前最新的 Go 版本(Go)下,这个选项对于自定义结构是不生效的。

举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func main() {
type Text struct {
ID int
Content string
}
type Message struct {
Name string
Content Text `json:"content,omitempty"`
}

var m Message
m.Name = "test"
bytes, err := json.Marshal(m)
if err != nil {
log.Fatal(err)
}
fmt.Printf("%s\n", string(bytes))
}

在上面的例子中,我们定义了一个结构体 Message,它包含一个字段 Content,类型为自定义的 Text,并打上 omitempty 选项。然后,声明一个实例并给 Message 的另一个字段赋值。接着序列化该实例。

期待序列化结果为 {"Name":"test"}。但是运行之后却得到以下结果:

1
{"Name":"test","content":{"ID":0,"Content":""}}

查看 encoding/json 相关代码会发现,该库在字段使用了 omitempty 选项时,对于空值的判断确实仅限于文档中描述的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// encode.go
func isEmptyValue(v reflect.Value) bool {
switch v.Kind() {
case reflect.Array, reflect.Map, reflect.Slice, reflect.String: // 空数组、空 Map、空 slice、空字符串
return v.Len() == 0
case reflect.Bool: // false
return !v.Bool()
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: // 0
return v.Int() == 0
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: // 0
return v.Uint() == 0
case reflect.Float32, reflect.Float64: // 0
return v.Float() == 0
case reflect.Interface, reflect.Ptr: // nil 指针、nil interface 值
return v.IsNil()
}
return false
}

也就是说,此判断逻辑不适用于自定义结构(非指针的情况下)。并且,除了在使用自定义结构的时候使用指针,没有其他任何方法可以让 omitempty 选项对自定义结构体生效!!

Issue 11939 提出并跟踪了这个问题。这个 Issue 从 2015 年 7 月份提出至今仍未有确定的解决时间 (`д´)

参考