精准踩中了 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 解析?要求
202
wwwuser 2023-09-21 17:53:11 +08:00
@zhs227
@BeautifulSoap @ye4tar 看了官方的文档避开那个坑就可以了,不需要改官方库,下面的代码可以校验原始 json 对象是否有 null 值,可以直接跑着测试一下,可能考虑不周全,可以一起完善下,献丑了 package main import ( "encoding/json" "fmt" ) func main() { data := []byte(`{ "id": 1, "name": "zhangSheng", "books": [{ "id": 1, "Name": 2, "desc": "golang book", "err": ["a","b"], "array": [{"a":"hello","b": null}] }], "desc": "test", "test": {"a": "s"} }`) var jsonMap map[string]interface{} if err := json.Unmarshal(data, &jsonMap); err != nil { fmt.Println(err.Error()) return } println(valueHasNil(jsonMap)) } func valueHasNil(mp map[string]interface{}) bool { for key, value := range mp { if value == nil { fmt.Printf("key: %s, value: %+v\n", key, value) return true } switch val := value.(type) { case []interface{}: for _, m := range val { switch t := m.(type) { case map[string]interface{}: if valueHasNil(t) { return true } } } case map[string]interface{}: if valueHasNil(val) { return true } } } return false } |
203
waitan 2023-09-21 18:05:49 +08:00
我是按这么理解:如果值为 null ,就代表这个字段没有值,那么就不做解析,使用结构体的零值,所以我做反序列化的时候,都会预防这一点,所以没踩过这一个坑。
|
204
gamexg 2023-09-22 03:34:57 +08:00
理解这个问题,也碰到过这个情况.
不过我是尽量让默认值就是符合默认情况 对于楼主说的金额这种无法确定默认值和未提供的情况,我会看情况使用指针 或 者把默认值定义为异常值,验证时发现是默认值直接报错. 当然对于金额这个允许 0 的情况,我会在 json 解析前就给结构提供一个正常情况下不允许的值,例如 -1 .这样如果客户端未提供金额,那么解析后金额值还会是 -1 , json 完成后验证下是否为负数完事,如果金额允许负数,那么我可能会将默认值设置为 0xFFFFFFFFFFFFFFFF 等业务上近乎不可能出现的值. 如果项目必须允许所有金额,那么只能设置为指针类型了. 其实这个问题出现的原因是,json 标准库没提供一个必须选项. 大概就是 golang 的大道至简?或者希望标准库只提供基本功能. 这种需求其实楼主可以自己自定义 json 来搞. |
205
gamexg 2023-09-22 03:47:12 +08:00
@gamexg 其实这个并不是简单的 null 解析不报错的问题.
如果楼主怕对外接口传递进来非法的 null 值被当作默认值是个问题, null 需要报错. 那么楼主考虑到如果外部根本没有传递这个字段会是什么情况? 一样是会被当作默认值. 对于 json 来讲却少字段是一个常见现象,会当作默认值也是常见现象,一样会造成楼主的问题. 其实说到底,这个问题就是 go 的 json 标准库没提供验证功能. 解决办法也只能要么换 json 库,要么自己自定义一个实现,或者用指针,或者提供一个会被标记为错误的默认值,验证时不允许这个值. |
206
gamexg 2023-09-22 04:01:25 +08:00
@snylonue
其实楼主真的纠结错地方了. 一个很好玩的地方, 楼主说无法控制调用者提交的内容需要解决外部输入的各种意外情况,所以必须检查可能为 null 的情况. 但是楼主看起来忘记了另外一个情况,即调用者未提供金额字段的情况. json 字段不齐全很正常,至少我知道的语言的 json 库默认都是允许的,不会报错.这时候金额还是会被设置为 0 . 会出现和 null 一样的问题. 当然有的 json 允许加上验证功能,这个字段必须提供,可以解决这个问题.不过 go 标准库的 json 没有提供,换库、自定义 json 、json 前提供一个错误的可被验证到的默认值、字段类型设置为指针、api 处一个结构内部一个结构 等办法都可以解决这个问题,就是需要增加工作量. 不过想要完整验证所有情况,必定会增加工作量.考虑仔细的代码很大一部分带阿妹都是在处理各种意外. |
207
harrozze 2023-09-22 09:24:01 +08:00
@ysc3839 #27
这个确实是。如果 go 的 json 标签可以支持类似 `notnull` 这种描述,对 null 报错,或者 `nullable`,允许解析成一个类似 `{ int value; bool isNull; }` 的结构体,用于区分 null 的 0 值还是真实 0 值,也能解决这个问题。 不然就只能是用指针类型了。反正……不改 go 的实现,就得改自己的代码(改用指针) |
208
zoharSoul 2023-09-22 10:26:40 +08:00
这个确实是个坑, 应该报错才合理的
|
209
snylonue 2023-09-23 23:01:19 +08:00
@gamexg
> json 字段不齐全很正常,至少我知道的语言的 json 库默认都是允许的, 我觉得这个按 null 处理比变成默认值更合理。另外 rust 的 `serde_json` 在这种情况下会报错(如果类型不是 `Option<T>`) |
210
Atsushi 2023-09-25 10:41:02 +08:00
@rekulas #50
我觉得这个问题换一个方向,不用传入值来匹配给定结构,用给定的结构来要要求传入值就说得通了。 我就是要求传入值和我给定的结构完全匹配,不能匹配抛异常。这应该是基本要求。 如果按 OP 所说只能给一个默认初始化值就太说不过去了。 |
211
EchoUtopia 2023-10-11 17:21:42 +08:00
确实是个问题,以前我写业务代码的时候对于需要区分 null 和零值的字段都是用的自己定义的 NullableInt 、NullableString 这类的类型。
|