在 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
23package 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 | func main() { |
运行,得到同样的错误: 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
15func 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
28type 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
26type 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
18func 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 | // encode.go |
也就是说,此判断逻辑不适用于自定义结构(非指针的情况下)。并且,除了在使用自定义结构的时候使用指针,没有其他任何方法可以让 omitempty
选项对自定义结构体生效!!
Issue 11939 提出并跟踪了这个问题。这个 Issue 从 2015 年 7 月份提出至今仍未有确定的解决时间 (`д´)