踩到 Go 的 json 解析坑了,如何才能严格解析 json?

233 天前
 BeautifulSoap

精准踩中了 json 解析包的两个坑导致了生产环境出错

假设有下面结构体定义

type Data struct {
	A   string `json:"a"`
	B   int   `json:"b`
	Obj struct {
		AA string `json:"aa"`
		BB int    `json:"bb"`
	} `json:"obj"`
}

使用json.Unmarshal() 解析下列几种 json

{"a":null, "b": null, "obj":null}
{"obj": null}
{"a": "a"}
{"a": "a","z":"z"}
{}
{"obj": {}}

问:解析哪个 json 会报错?

答:全都不报错都正确解析

都是不出事就注意不到的问题。尤其非指针类型字段,我下意识认为遇到 null 是会直接报错的,结果直接是当作不存在(undefined)来处理。。。

so ,go 下怎么才能简单地进行严格 json 解析?要求

  1. 不允许出现未知字段,出现则报错(这个似乎倒是可以用 json 包的 DisallowUnknownFields 简单做到)
  2. 非指针字段不允许传入 null ,否则报错(似乎 json 包没法简单做到)
12786 次点击
所在节点    Go 编程语言
211 条回复
zhs227
232 天前
我看过结构体成员用指针判断是否空串的,不清楚是否满足你的要求
BeautifulSoap
232 天前
@zhs227 可能不太现实,因为通过定义成指针来满足第二点需求的话,意味着结构体全部字段都必须定义成指针。如果字段非常多(几十上上百个)一个个判断代码量和工作量非常大增减字段容易出纰漏(最终要上反射)。而且所有字段定义成指针的话,使用起来会相当难受
fgwmlhdkkkw
232 天前
BeautifulSoap
232 天前
@fgwmlhdkkkw json.Unmarshal() 会把 null 解析成对应类型的空值,比如 int 的话就是 0 。validation 只能判断是不是 0 不能判断是不是 null ,派不上用场
LLaMA2
232 天前
声明的时候这样
A string `json:"a,notnull"`
B int `json:"b,required"`

试试看
BeautifulSoap
232 天前
@ye4tar 试了下没效果。json 包其实不不支持 notnull, required 标签的
LLaMA2
232 天前
tuxz
232 天前
对这个结构体实现自定义的 UnmarshalJSON 方法就行了
RedisMasterNode
232 天前
我出两个小建议,但是可能都需要对全局代码进行查找和替换。不过改动量还算可控的。

1. 如果只是针对 json 格式和 struct 定义不完全匹配,可以用 jsoniter 库,通过这个配置玩一下:jsoniter.Config{DisallowUnknownFields: true}
2. 如果需要在 struct 上自定义 tag ,例如:required ,那可以提供一套自定义的 json 方法,里面先使用 github.com/go-playground/validator/v10 进行检查(支持的 tag 应该足够用了),再执行原生的 json 方法,方法签名保持不变

对于方案 2 ,全局的代码应该可以通过修改 import ,把所有的 json 包都替换成自行实现的 json 包,提供 marshal / unmarshal 方法就可以了
pkoukk
232 天前
你要的是数据验证,而不是 json 反序列化,应该用这个
https://github.com/go-playground/validator
lshang
232 天前
试一下先解析成 map[string]any,然后用 mapstructure 解析?
haoxue
232 天前
Maboroshii
232 天前
如果已知字段会为 null ,直接用指针吧。
RedisMasterNode
232 天前
https://go.dev/play/p/8_2sAiXdrQ7

9L 的小 demo ,试了一下感觉应该挺好用的

type User struct {
Name string `json:"name" validate:"required"`
Age int64 `json:"age" validate:"required"`
}

testCase: {"name": "john", "age": 10}
result: <nil>

testCase: {"name": "john", "age": 10, "is_v2ex": false}
result: main.User.ReadObject: found unknown field: is_v2ex, error found in #10 byte of ...| "is_v2ex": false}|..., bigger context ...|{"name": "john", "age": 10, "is_v2ex": false}|...

testCase: {"name": "john"}
result: Key: 'User.Age' Error:Field validation for 'Age' failed on the 'required' tag
dobelee
232 天前
你搞错了,这是数据检验需求。
knightdf
232 天前
这就是 json 解析的正常结果啊,你的需求是 validation
rekulas
232 天前
"因为 json 默认把 null 解析为空值,所以解析 json 的时候并不会报错,商品价格会以 0 元被解析"
作为一个合格的程序员会告诉你,并不会,null 本来就是合格的 json 值格式你为何非要别人报错

当某个问题别人从没遇到过自己经常遇到的时候,好好思考下,会不会是自己的思维方式或使用方式不当
BeautifulSoap
232 天前
@knightdf
@dobelee
@pkoukk
关于 validation 这件事,你们真的应该亲自试试,就会发现这根本不是 validation 能解决的问题。
null 会被解析为默认空值,如 int 字段传入 null 会被解析为 0 ,即便用 validator 这个包做 validation 检测也只能检测字段是不是 0 。但在实际业务中 int 值字段为 0 基本都是正常值,不应该被报错


@RedisMasterNode
你理解错 9L 的意思了,你提供的这段代码其实是有问题的。比如你尝试解析下 `{"name": "john", "age": 0}` 是会报错的(有的地区是有 0 岁这个概念的哦),单纯在解析后用 validator 是没法区分传入的到底是 0 岁还是 null 的
BeautifulSoap
232 天前
@rekulas 如果你是个合格的程序员并且经验丰富接触的语言也不只有一种的话,那么你会清楚一般来说语言解析遇到 null 的话,都会尽量将其解析为对应语言中的空指针/null/None 一类。比如 python 把 null 解析为 None, js 、php 、java (大部分 json 包)会把 null 解析为 null 。而对于 kotlin 这种明确区分了空值非空值的语言,将 null 解析为非空类型的字段会直接报错。

话题绕回来,对于 json 来说 null 存在是合理的,但是将 null 解析为非指针的时候不报错解析为默认空值是不合理的。请问你如何区分 {"a":null} {"a":0} ?而且落地到实际项目中,假设一个 api 接口所有字段都是禁止传 null 的,但是前端/外部接口就硬是因为 bug 之类的给你传了个 null ,请问你认为是直接报错拒绝请求比较好呢,还是直接解析成对应类型空值去处理业务从而引发严重事故好?
tairan2006
232 天前
你可以你可以用代码判断啊…匿名类那里改成指针,然后代码判断…

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/975214

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX