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

2023-09-19 15:28:01 +08:00
 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 包没法简单做到)
15397 次点击
所在节点    Go 编程语言
211 条回复
lanlanye
2023-09-20 10:12:52 +08:00
Go 默认不使用指针而是零值的原因大概和 protocol buffer 一样。
按照我的理解,业务上要求你区分空值和零值,这个判断就应该你自己来做。基本类型想要精确捕获空值的话就是要么定义成指针,要么参考 Go 的 sql.NullTime 之类的结构实现类似的东西。

或者,换一种语言会舒服很多。
leonshaw
2023-09-20 10:21:21 +08:00
看了一下 op 的 append ,有一个误区。并不是 null 解析成了零值,而是 encoding/json 不区分 null 和 undefined ,传 null 不会改变原来的值。
调用方传 null 和不传是一样的,如果允许不传,就一定有一个默认值( 0 或非 0 ),那就在 Unmarshal 之前设置默认值;如果不允许不传,那就用其它方法来校验。
InkStone
2023-09-20 10:23:53 +08:00
@Jammar 坑的地方在于这种解析的语义是错误的。如果你这么多年一直都是这么解析的,说明一直都是错误的。

把 json::null 和 json::int::0 映射都映射到 go::int::0 ,不管从什么角度看都不可能是正确的行为。
aababc
2023-09-20 10:29:43 +08:00
@InkStone goer:你不懂 go 理念的先进之处🐶
pkoukk
2023-09-20 10:36:19 +08:00
@BeautifulSoap
为什么年龄会有 null 呢?如果是必填数据,人还没出生这个人的数据从哪来的?
如果这是个非必填数据,使用*int 又有什么问题呢?
感觉很多写 go 的人非常排斥使用*string 或者*int ,想不通为什么
InkStone
2023-09-20 10:41:12 +08:00
@pkoukk 任何语义上不可能有 null 文档也规定了不可以有 null 的字段,在实际传过来的时候都可能有 null 。这对后端来说应该是显而易见的事情……
Nugine0
2023-09-20 10:42:46 +08:00
@rekulas #38 关 rust 什么事……rust 的 serde_json 库就是 op 想要的效果
qinyui
2023-09-20 10:44:09 +08:00
```go
var a Data

a.B = -1
a.Obj.BB = -1

data := `{"a": "a"}`
_ = json.Unmarshal([]byte(data), &a)
fmt.Println(a.A)
fmt.Println(a.B)
fmt.Println(a.Obj.AA)
fmt.Println(a.Obj.BB)
```
在解析前将 int 字段的默认值修改为-1 实现 null 的效果用来跟 0 区分,业务判断等于-1 说明字段没传,算不算一种思路?
guanzhangzhang
2023-09-20 10:49:41 +08:00
零值这个没办法,go 就是这样,json 和 yaml 里 null 就是空,go 里就变成零值了
pkoukk
2023-09-20 10:50:22 +08:00
@InkStone
这还是需要区分讨论的,如果我是个内部的 grpc 服务/消息队列消费者,我不会对这种奇怪的情况做兼容
如果我是个面向外部的 HTTP 服务,我不可能直接用 decode http body 的 struct ,让它透传到数据库或消息队列
楼主的问题我觉得很好理解,他接受请求用的 struct 用在了后面的很多处理流程里,没有一个“消毒区”,把前端进来的数据处理成面向系统内部流程的 struct ,导致他出现了这么多痛苦的情况
如果本身接收外部请求的是一个专用 struct ,里面可以包含很多{A *int}这类为了检验而存在的妥协,校验完成后新组成的对象就完全可以是 {A int}了啊
p1gd0g
2023-09-20 10:55:47 +08:00
遇到过,但是当时没觉得是个问题
kkbblzq
2023-09-20 10:57:42 +08:00
这其实无关 json 解析,非指针的默认值的逻辑充斥着 go 的每个地方,你可以说不好,但是的确就是特性;如果你想在解析的时候直接报错,你可以自定义个 NotNullInt ,实现一下 UnmarshalJSON ;至于你说的指针满天飞,实际情况下真有那么多字段需要区分吗?如果这些字段真这么严格的话,处理一下 npe 很难吗?
LuoyeBug
2023-09-20 11:00:29 +08:00
我们是自己写了一个解析的,里面加了 tag 来判断要执行什么操作。
ruoge3s
2023-09-20 11:02:37 +08:00
换个更喜欢的语言用吧~
pkoukk
2023-09-20 11:07:07 +08:00
如果你接收到的数据有很大程度的不确定性,那么你做的第一件事应该是消除掉这个不确定性
我理解你希望 json 包帮你解决这个问题,但它没有不是因为它有问题,而是它要服从于 go 的设计逻辑
在 go 里,明确有*int 这样的指针来判断这个参数的 “有”和“无”,所以 json 包没必要越俎代庖,否则也会有另一批人跳出来说它有毛病
你现在的问题很简单,用*int 能解决你的校验问题,但你不愿意用,因为对后续其它逻辑处理流程太麻烦
我们的解决方案很简单,你只要多加一层 struct 就行了,这层 struct 和你现在 struct 的区别在于,那些不能接受默认值的字段,改成指针
用这个校验 struct 对进来的数据进行校验,校验通过复制到逻辑 struct 里去,后面的流程不变
realJamespond
2023-09-20 11:08:27 +08:00
不用指针就表示要分配 struct 实际内存空间,这个懂一点 c 就知道的,如果是 c 还要手动 memset 成 0 ,现在 go 已经帮你置 0 分配置的内存了还不行么
MoYi123
2023-09-20 11:18:31 +08:00
https://gist.github.com/WonderfulSoap/18a14da135f659d5350f36bdbe439b6a

可能是 null 的用指针, 不可能的用值, 全用指针干什么?
就算用 optional, 也要判断 has_value()啊, 指针也就多一次寻址, 性能差一点, 代码写起来又差不多的.
AItsuki
2023-09-20 11:25:56 +08:00
我觉得大家审题有问题,楼主是想要一种严格的 json 解析方式,非空类型遇到空值应该报错。例如 java 的 gson 就支持。
我现在的想法是能不能使用自定义 json 来处理,某个类型的字节数组为空就报错,有没有人尝试一下。
yidadaa
2023-09-20 11:37:18 +08:00
你需要区分 decoder 和 validator ,有的库会同时提供两个能力,但有的库只会提供 decoder 能力,你去找个带 validator 的就行了
gps949
2023-09-20 11:39:08 +08:00
omitempty

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

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

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

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

© 2021 V2EX