踩到 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 包没法简单做到)
14213 次点击
所在节点    Go 编程语言
211 条回复
gogogo1203
2023-09-20 09:30:00 +08:00
@leoleoasd
json required tag 可以校验 not present
tsanie
2023-09-20 09:30:33 +08:00
不过回到例子中来,op 这里应该把 B 与 Obj.BB 设置为 nullable int
gogogo1203
2023-09-20 09:34:09 +08:00
@tianxin8431 哈哈哈。 “只要我不同意的观点”===“学院派”。 业务上根本不难受,写一次就够。非要“公牛挤牛奶”,自己给自己找难受罢了。
zliea
2023-09-20 09:36:31 +08:00
go 的 json 反序列化 null 值或者空值不报错或者赋默认值这是一个特性,既然你用了别人的库就要接受相关特性,因为这是一个特性所以你的需求需要自行实现校验。

现在来看解决方案有两种,第一种通过指针,第二种自定义 type + 实现 UnmarshalJSON 。
skiy
2023-09-20 09:38:41 +08:00
@aloxaf

{A: B:0 Obj:{AA: BB:0}}

我亲测的没报错啊:
![]( https://file.uhsea.com/2309/81654763b2762f4e551d7c06c6f388c3XL.png)

```
package main

import (
"encoding/json"
"fmt"
)

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

func main() {
var json_str = `{"a":123, "b": "abc", "obj":999}`
var test Data
json.Unmarshal([]byte(json_str), &test)
fmt.Printf("%+v", test)
}
```
aloxaf
2023-09-20 09:39:35 +08:00
@tianxin8431 没懂,学院派都是追求正确性的。学院派连 null 都唾弃,现在还把 null 转换到正常类型的空值上去,学院派简直震怒好吗……
skiy
2023-09-20 09:40:12 +08:00
kumoocat
2023-09-20 09:43:58 +08:00
@skiy Unmarshal 的异常是有的,没有 panic
SingeeKing
2023-09-20 09:44:18 +08:00
目前看用标准库的话,确实没办法对 null 报错,传 null 行为和不传是一样的

不过想要区分不传(或传 null )和传值的方法很明确:使用 `*int`
skiy
2023-09-20 09:45:54 +08:00
@aloxaf 好吧。我没加判断。确实 err 了,不过解析的结果确实如上。
wzy44944
2023-09-20 09:48:39 +08:00
看到第一个 json 第一反应是这个不应该是非法的吗,null 都没有""包裹,然后查了下才发现默认 json 是支持 null 的,https://www.json.org/json-zh.html ,因为 json 就是 javascript 来的。。。
不过我还真没遇到这种问题,写 go 习惯了,都是用下面这种代码检查默认值,可能我用 json 不多,不嫌麻烦吧,主要是有的时候可以免去检查 err 了
var obj = &Obj{ val: -1}
_ = json.Unmarshal(data, obj)
if obj.val == -1 {
panic("haha")
}
goool
2023-09-20 09:49:21 +08:00
似乎是数据检查的事情,可以考虑 json schema https://json-schema.org/
skiy
2023-09-20 09:51:49 +08:00
@kumoocat 我理解 OP 的意思了。他说的是用 null ,没报错。

所以我站在 OP 这边,哪怕你下面有人说官方有解释。但 null 本来就不是 go 的关键字。
tool2d
2023-09-20 09:54:00 +08:00
null 还是很重要的基础类型之一,你看别的语言解析,一般都会保留一个变体字段结构,来识别原始 JSON 字段的具体类型。

如果强制映射到结构体,那么 null 就会消失,变成 int 或者 string 默认值,也是能理解的。

指针 nil 是个解决方案,也没有更好的办法了。
picone
2023-09-20 09:54:17 +08:00
@ye4tar #5 go 的 validator 是在 json Unmarshal 之后进行的。Unmarshal 已经丢掉了是否 null 的信息了
Leviathann
2023-09-20 09:58:05 +08:00
@tianxin8431 说 go 是学院派你是不是搞错了什么,学院派在 21 世纪做出没有泛型的静态类型语言那真是要被钉上耻辱柱
ForkNMB
2023-09-20 10:02:36 +08:00
传输协议用的 protobuf go 后端同样遇到这样的问题 解析出来 int 的值是 0 无法确认客户端传的是 null 还是就是业务上的 0 ,所以全改成指针了,然后全局替换判断,取值的时候也用公共方法统一替换。这样客户端不用改,就后端加了一堆工作量,md 好想写回 java ,加个注解就完事了🤔
gam2046
2023-09-20 10:03:06 +08:00
其实问题在于 JSON 并没有指针的概念,null 是一个合法的值,而 Golang 中在进行反序列化时,没有与 null 相对于的值用于表示这个状态,同样的,也没有对应于 undefined 状态。

一种凑合的解决方案呢,就是业务中不使用默认值作为合法状态,例如 0 元的价格 price ,当值为-1 时代表 0 元,而 0 代表未传值。类似的,空字符串等都不作为有效的数据输入,用其他值作为默认值的替代。

但是多数场景下,op 的问题可能都不大,0/""这些默认值都不具有实际意义。
picone
2023-09-20 10:07:10 +08:00
我觉得其实和团队风格有关。我以前的公司 toC 高并发,比较抠性能不抠细节。现在的公司 toB ,反正业务维护得下去就行性能无所谓。然后我才接触到,json 里还区分 not assign 和 null 有区别的。谷歌说的 often 其实在大部分情况是成立的,比如 10 个字段 9 个不区分这样。 作为一种语言,把自己的理念强行安排给所有人这事 Go 干的不少。
说回现在我们是怎么解决的,就是多包一个类似 Optional 的实现,并且实现对应的自己的 validator 。完全可以实现楼主所说的功能。在泛型没出来之前是每个基础类型包一下,幸好现在出来了,但是历史代码还是有点不雅。
Jammar
2023-09-20 10:08:16 +08:00
哪里坑了?这么多年不一直是这么解析?有时候多找找自己的原因好吧,有没有认真学习,技能有没有提升

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

https://tanronggui.xyz/t/975214

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

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

© 2021 V2EX