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

2023 年 9 月 19 日
 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 包没法简单做到)
17779 次点击
所在节点    Go 编程语言
211 条回复
thevita
2023 年 9 月 21 日
@thevita 是指 golang 的 sql 包
wwwuser
2023 年 9 月 21 日
@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
}
waitan
2023 年 9 月 21 日
我是按这么理解:如果值为 null ,就代表这个字段没有值,那么就不做解析,使用结构体的零值,所以我做反序列化的时候,都会预防这一点,所以没踩过这一个坑。
gamexg
2023 年 9 月 22 日
理解这个问题,也碰到过这个情况.
不过我是尽量让默认值就是符合默认情况
对于楼主说的金额这种无法确定默认值和未提供的情况,我会看情况使用指针 或 者把默认值定义为异常值,验证时发现是默认值直接报错.
当然对于金额这个允许 0 的情况,我会在 json 解析前就给结构提供一个正常情况下不允许的值,例如 -1 .这样如果客户端未提供金额,那么解析后金额值还会是 -1 , json 完成后验证下是否为负数完事,如果金额允许负数,那么我可能会将默认值设置为 0xFFFFFFFFFFFFFFFF 等业务上近乎不可能出现的值. 如果项目必须允许所有金额,那么只能设置为指针类型了.




其实这个问题出现的原因是,json 标准库没提供一个必须选项.
大概就是 golang 的大道至简?或者希望标准库只提供基本功能.
这种需求其实楼主可以自己自定义 json 来搞.
gamexg
2023 年 9 月 22 日
@gamexg 其实这个并不是简单的 null 解析不报错的问题.
如果楼主怕对外接口传递进来非法的 null 值被当作默认值是个问题, null 需要报错.
那么楼主考虑到如果外部根本没有传递这个字段会是什么情况? 一样是会被当作默认值. 对于 json 来讲却少字段是一个常见现象,会当作默认值也是常见现象,一样会造成楼主的问题.

其实说到底,这个问题就是 go 的 json 标准库没提供验证功能.
解决办法也只能要么换 json 库,要么自己自定义一个实现,或者用指针,或者提供一个会被标记为错误的默认值,验证时不允许这个值.
gamexg
2023 年 9 月 22 日
@snylonue

其实楼主真的纠结错地方了.
一个很好玩的地方,
楼主说无法控制调用者提交的内容需要解决外部输入的各种意外情况,所以必须检查可能为 null 的情况.
但是楼主看起来忘记了另外一个情况,即调用者未提供金额字段的情况. json 字段不齐全很正常,至少我知道的语言的 json 库默认都是允许的,不会报错.这时候金额还是会被设置为 0 . 会出现和 null 一样的问题.

当然有的 json 允许加上验证功能,这个字段必须提供,可以解决这个问题.不过 go 标准库的 json 没有提供,换库、自定义 json 、json 前提供一个错误的可被验证到的默认值、字段类型设置为指针、api 处一个结构内部一个结构 等办法都可以解决这个问题,就是需要增加工作量.
不过想要完整验证所有情况,必定会增加工作量.考虑仔细的代码很大一部分带阿妹都是在处理各种意外.
harrozze
2023 年 9 月 22 日
@ysc3839 #27

这个确实是。如果 go 的 json 标签可以支持类似 `notnull` 这种描述,对 null 报错,或者 `nullable`,允许解析成一个类似 `{ int value; bool isNull; }` 的结构体,用于区分 null 的 0 值还是真实 0 值,也能解决这个问题。

不然就只能是用指针类型了。反正……不改 go 的实现,就得改自己的代码(改用指针)
zoharSoul
2023 年 9 月 22 日
这个确实是个坑, 应该报错才合理的
snylonue
2023 年 9 月 23 日
@gamexg
> json 字段不齐全很正常,至少我知道的语言的 json 库默认都是允许的,

我觉得这个按 null 处理比变成默认值更合理。另外 rust 的 `serde_json` 在这种情况下会报错(如果类型不是 `Option<T>`)
Atsushi
2023 年 9 月 25 日
@rekulas #50
我觉得这个问题换一个方向,不用传入值来匹配给定结构,用给定的结构来要要求传入值就说得通了。
我就是要求传入值和我给定的结构完全匹配,不能匹配抛异常。这应该是基本要求。
如果按 OP 所说只能给一个默认初始化值就太说不过去了。
EchoUtopia
2023 年 10 月 11 日
确实是个问题,以前我写业务代码的时候对于需要区分 null 和零值的字段都是用的自己定义的 NullableInt 、NullableString 这类的类型。

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

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

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

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

© 2021 V2EX