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

257 天前
 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 包没法简单做到)
12949 次点击
所在节点    Go 编程语言
211 条回复
aababc
257 天前
@Yourshell 这个提案也解决不了这个问题
BeautifulSoap
257 天前
@rekulas 我不知道是自己说明能力太差还是你没法理解,我举例 python 、php 、js 只是想说他们都会将 json 中的 null 解析成对应语言中的 None/null 。这点你能明白吗?然后在 Go 语言里,与 json 的 null 最接近的概念也就只有 nil 了,你同不同意?所以将 null 的解析为 nil → 而非指针类型无法赋值 nil 从而会引发解析错误难道不是非常自然的想法?为什么你会认为这是个校验的问题呢?

而且如果要较真的话,我们再来深入思考一个问题,什么才叫校验?你说 null 解析成 nil 赋值给非指针字段是校验问题,那么解析如下 json {"a":"hello"}到 这个结构体 struct { A int} 是不是也应该归类为校验问题? 在这里 A 是 int 类型,但是传入了字符串,两者的类型不对的检测是不是也应该交给校验?按照你的说法,解析 { "a": "hello"} 的结果就应该和{"a": null} 一样都解析成 0 ,然后交给校验来处理是不是?
houshuu
257 天前
@BeautifulSoap #31

1. 我还以为你是不想把事故说很细, 换了个类似讲法规避一下风险. 但话说回来, 如果连测试都不需要, 那么这个业务 SLA 估计也没啥要求, 这种问题就算遇上也就是个慢慢修的过程.

2. 如果"需要对传入的数据做最低限度的确认", 那么就应该用指针. 因为这句话的背后已经说明了有可能为 null.

3. 不是自己的锅别接, 别人再怎么说也改变不了这个不是后端的问题. 我不知道是不是只有我司这样, 数据处理时要不要检查 null 或是其他异常值永远都是白纸黑字写的清清楚楚的. 不同语言对于异常值的处理差别太大了, 有的在 parse 时报错, 有的要用的时候才报错, 有的像 go 是在内存申请后自动 0 值的, 有的是不初始化的. 不写清楚太容易出问题了.

你现在就是一定要用 int 去完成指针的功能, 那除了自己魔改或者你自己做点 optional 的类型, 确实没啥办法了. 还是要用合适的工具去做合适的事情.
BeautifulSoap
257 天前
@lshang 你说的非常对,你说的这个情况其实就是我顶楼给的几个例子中之一

解析下面的 json 都会成功,解析结果就是所有没有填的字段全为空值

{"a": "a"}
{}
{"obj": {}}
debuggerx
257 天前
支持 op ,go 的包括 json 这块在内的很多设计其实都很扯,错的离谱还扯什么大道至简,一边标榜自己新,一边又只敢跟传统语言对比,对其他新语言的优秀特性视而不见听而不闻,可以说是非常双标了
Goooooos
257 天前
@kumoocat 这种场景用 Java 来说 go 没问题是不对的,因为 Java 可以定义包装类型 Integer
seers
257 天前
这个设计确实比较离谱
BeautifulSoap
257 天前
@houshuu
> 如果"需要对传入的数据做最低限度的确认", 那么就应该用指针. 因为这句话的背后已经说明了有可能为 null

你好,你这已经是我针对不同人提出的同一个问题的第三次回复了。定义成指针是没有可行性的做法。理由很简单,作为后端我必须不相信前端,所以我要考虑 DTO 的所有字段都有可能被前端瞎传入 null 的情况。
如果采用指针的话,这意味着我一切 struct 中每一个字段都必须定义成指针。这根本不现实对吧?整个项目 DTO 中所有字段全是指针用起来一定很酸爽对吧。再一个,实际项目中 API 并不是所有字段都禁 null ,个别字段是允许 null 的,所有一切字段都定义成指针的话,请问你能轻松分辨出这一堆指针类型的字段中,哪些是允许 null 哪些是禁止 null 的?更要命的是,使用字段值的时候到底哪个字段要加*,哪个字段要判 nil ?

> 如果连测试都不需要

不好意思,不是没有测试而是测试没有覆盖到。你如果认为所有项目只存在完全没测试和所有测试条件都覆盖到的两种极端的话那就太理想了

> 不是自己的锅别接, 别人再怎么说也改变不了这个不是后端的问题

不是的,锅的确不是自己的我也不会接,但是因此产生的生产事故和因为事故调查、恢复加的班是实打实(你不可能叫前端过来给你查后端 l 日志,抽取数据回复被破坏的生产环境)
lance6716
257 天前
离谱,你说 often 就 often

https://pkg.go.dev/encoding/json#Unmarshal

Because null is often used in JSON to mean “not present,” unmarshaling a JSON null into any other Go type has no effect on the value and produces no error.
rekulas
257 天前
先说答案: 我认为解析成 0 没问题, 错就错在你自己定义类型不对

go 本身提供了非常好的解决方案 *指针, 稍微有点基础的都知道灵活运用来判断空(默认)值与 nil 值的区别
比如接收参数的时候,如果预判参数可能超过范围或可能存在空,直接指针介入即可
或者读取数据库的时候,也可以用来判断

通过这种方式来区分 nil 与空值,我认为非常合理,不应该在解析阶段处理,如果在解析阶段处理,我才觉得会引起大麻烦:
我目前接手的一些第三方类库一些结构体参数就几十个,很多值不需要额外处理直接默认留空即可,如果按你的想法,那漏掉一个 kv 对不传输过来都会报错

另外 你如何看待 c 和 rust 对于这个的处理
leonshaw
257 天前
op 说的有一定道理,但是可以尝试从另一个角度考虑。对于许多语言来说 {"a": null} 和 {} 是等价的,类比到 Go, 不传这个字段就是 0 值是合理的。所以实际上缺少的是一个 "required" tag, 当有 required 时,要求不能是 null.
rekulas
257 天前
我能理解你在判断 nil 与空值的模糊感受,但仍然不会赞同你的想法,在我看来,语言框架基础方法就应该尽可能简洁低耦合,要实现的功能完全可以通过三方库而且性能还不低

比如 go-playground/validator
如果我要验证必须有值 加 required
如果我要求某项值小于 30 加 lte=30
如果我要求 enum 验证 加 oneof=xiaoming huahua
如果我要求字符串以 V2EX 开头 加 startswith=v2ex



如果各种方便的方法都糅杂在 go 自身,那画面简直不敢想象
gogogo1203
257 天前
是我迷糊了吗? 零值不是 创建 struct 的时候就有了吗??“The JSON null value unmarshals into an interface, map, pointer, or slice by setting that Go value to nil. Because null is often used in JSON to mean “not present,” unmarshaling a JSON null into any other Go type has no effect on the value and produces no error.” 自己读一读 doc 呗,unmarshaler 只是没有去改原先的零值而已。unmarshaler 要快,而不是提前做很多 validation.这些后续你可以慢慢搞。
nuk
257 天前
可以在 Unmarshal 检查一下 json ,给 struct 加 tag ,然后自己写个 scaner 。老实说 golang 的 json 我也感觉不太好用,看似简单实则臃肿。
BeautifulSoap
257 天前
@rekulas 典型站着说话不腰疼。我写了个例子给你展示所有字段都为指针是什么地狱情况
https://gist.github.com/WonderfulSoap/18a14da135f659d5350f36bdbe439b6a
真的太优雅了,是吧。main2.go 用来模拟在其他包里调用 Data2 数据的情况。悄悄跟你说 Calculate() 里已经塞入了两个 bug 了哦? data.I 和 data.Obj1.I 是可 null 字段段,所以不能直接取值。那么问题来了,如此高的心智负担这么多指针倒来倒去,你怎么确保你每次取值都取得万无一失?
rekulas
257 天前
@BeautifulSoap 为了证明自己是对的 刻意写了个极端情况 真是辛苦你了
可是我发现你逻辑思维可能有些问题,具体表现在:
非要想靠语言自身 hold 所有情况
对别人提出的问题视而不见, 只想强行解释自己的想法
BeautifulSoap
257 天前
@rekulas 我觉得你反倒很令人费解,请问你真的有业务经验吗,我说的都是实实在在经手的项目中遇到的问题。实际复杂业务中,前端/外部接口根据需要传过来的 json 包含这么多字段不是很正常的?其中个别字段为可 null 难道不正常?我给你的这个 Data2 就是我目前一个项目的 DTO 定义(当然类型并非全都是 int ,数量规模差不多这种感觉)

作为后端的基本要求就是不信任外部输入,我就只能考虑以这些字段都可能被传入 null 。你说全定义成指针可以解决问题,那么我就定义成指针了,结果就是我给的例子中的样子为什么反倒说我极端。。。。而且你要知道一旦涉及到业务,也就不是例子里简简单单的取值相乘了,根据 nil 不 nil ,计算方法不同等等等等一大堆复杂计算,全都是要对这些指针倒腾来倒腾去的,所以我才说不现实
twl007
257 天前
@BeautifulSoap 可以参考下 GRPC 的做法
https://zhuanlan.zhihu.com/p/46603988?utm_id=0

额外给你认为一定要区分的变量配置一个是否为 null 的 bool 的字段

文章里也给了一些讨论的原因 为什么这么设计 感觉跟 json 这块的设计还有些共通的地方吧

https://stackoverflow.com/questions/31801257/why-required-and-optional-is-removed-in-protocol-buffers-3
BeautifulSoap
257 天前
@gogogo1203 这就涉及到 49L 吐槽的的核心内容了。这 often 真的是 often 吗
leoleoasd
257 天前
感觉 json 库这么搞的问题是没法区分 json 里写了 0 还是写的 null 还是 undefined

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

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

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

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

© 2021 V2EX