踩到 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 包没法简单做到)
15405 次点击
所在节点    Go 编程语言
211 条回复
leochenL
2023-09-20 17:06:17 +08:00
@BeautifulSoap 说说咋解决的
yingyan25
2023-09-20 17:36:45 +08:00
@lshang 我之前也是碰到 OP 这个问题,采用的也是这种方法
BeautifulSoap
2023-09-20 17:38:08 +08:00
@gogogo1203 这个帖子最初就一个目的:问怎么解决 null 解析问题,然后这个问题在前 20L 前已经被解决了。

但接下来我要说的话就不客气了

但是你要不要仔细想想猜为什么后来会发展为吐槽?因为我是真的被回复中的几位整烦了。
这个帖子 20 多楼之后除了少数几个回答可以看出真的有经验,回答是有用的之外,是不是一大堆自以为是、好为人师人的人一副教育人的口吻在提方案?提的方案还暴露了他们根本就在这方面毫无经验这回事。我就问,这贴已经有多少人说 null 解析是 validation 的问题?而这些说 validation 的人有多少人是能想到 null 解析是根本无法靠 validation 解决的?还出现了好几位“测试干什么”的主。更有说让人把一切字段定义成指针这么离谱方案的,最后被我拿例子怼眼前说在项目中用指针不现实反倒没下文了。

再比如说你,你自己扪心自问,你在 29L 和 35L 代码是不是展示了你最开始甚至连 json 解析 null 会出问题都没注意到?也不知道 null 解析成 0 会导致无法 validation ?我来猜一猜,你是不是被我指出有问题之后为了反驳我才去仔细读了 json 官方文档,才第一次知道 json 解析 null 值会有”我们认为 null 经常代表空所以跳过赋值”这一特性?
就好像上面好几位都在说“int 不是指针所以遇到 null 赋值 0 是合理的”一样,说这话就证明根本不知道是怎么回事硬要来教育人是不是?
PS:你再仔细看看我顶楼里的怎么说的 “尤其非指针类型字段,我下意识认为遇到 null 是会直接报错的,结果直接是当作不存在(undefined)来处理。。。”仔细想想
BeautifulSoap
2023-09-20 17:57:25 +08:00
@leochenL 21L 其实就说了,直接复制出 json 包改了判断部分
https://cs.opensource.google/go/go/+/master:src/encoding/json/decode.go;l=885
这一行是解析 null 的部分,内部的 switch v.Kind() 就是在判断解析对象字段的类型。目标字段为非指针、map 这些不可设置 nil 的类型直接报错就行了

前后就花了不到十来分钟,然后写了单元测试后全局替换掉包引用,扔到开发环境慢慢等测试开始摸鱼了。所以才有这么多时间在这贴里和人对线。等啥时候高兴了再加个 flag 用于开关这个特性会更好点。不过目前这严格解析已经够用了。
唯一的问题就是因为复制的 Go 1.21 的 json 包,今后 go 升版本的时候标准包有变化的话得跟上(不过问题应该不大,大不了把这个包搞成公司内通用 repo )
gogogo1203
2023-09-20 18:56:15 +08:00
@BeautifulSoap 你还不客气了. 麻烦你去看看 unmarshal 什么时候回解析成零值。unmarshal 从来不会给你的 struct 复零值, 碰到 null 它不会处理而已。
================================================================
你还不客气了你还不客气了你还不客气了你还不客气了你还不客气了你还不客气了你还不客气了你还不客气了你还不客气了你还不客气了你还不客气了。

================================================================
零值是你创建的时候的默认值。。。。。。跟 unmarshal 无关

================================================================
你从昨天对线到现在, 从抱怨 unmarshal 因为碰到”not present“不报错, 接着开始抱怨"not present" 和“zero value”无法快速区分。哪个都不是 unmarshal 锅。

===============================================================
别人用 go 写 curd 都写了多久了, 到你这里连个 json 都没有搞明白。 还开贴对线。 得了吧你。
gogogo1203
2023-09-20 18:58:16 +08:00
你接着对线了, 我非常想看看这个帖子能更新到什么时候。最好是能火到 go team 过来跟你道歉。
ding2dong
2023-09-20 19:43:52 +08:00
null 就是对应 go 的 nil ,有什么问题吗?你的问题是要加验证,而不是寄希望解析规则为你而改变,反正我认为不是坑
ClericPy
2023-09-20 22:14:17 +08:00
非 golang 开发, 犹记得当初刚开始用 GO 解析 JSON 被折腾的太痛苦了.

当时用了好几年 JMESPath, 忘了这玩意居然支持 go 语言, 走了半天弯路最后换语言了 https://jmespath.org/libraries.html
snylonue
2023-09-20 22:14:28 +08:00
不明白为什么那么多人说是验证的问题,`{ "int_field": 0 }` 和 `{ "int_field": null }` 解析成一个结果明显是丢掉了信息
kiwi95
2023-09-20 22:27:35 +08:00
楼里很多人确实理解错 OP 的问题了,但是楼主因为这个需求魔改 json 包的做法是在难以认同,这不是又给自己挖一个更大的坑,直接问 chatgpt 得到的方案都更好。

When using `json.Unmarshal` in Go, you can define a custom struct type that matches the structure of the JSON data you want to parse. You can also define custom types that implement the `json.Unmarshaler` interface to handle more complex parsing scenarios.

In your case, you can define a custom type for the `int` field that can handle the `null` value. Here's an example:

```go
type NullableInt struct {
Value int
Valid bool
}

func (ni *NullableInt) UnmarshalJSON(data []byte) error {
if string(data) == "null" {
ni.Valid = false
return nil
}
err := json.Unmarshal(data, &ni.Value)
if err != nil {
return err
}
ni.Valid = true
return nil
}
```

In this example, we define a `NullableInt` type that has two fields: `Value` and `Valid`. `Value` holds the integer value if it is not `null`, and `Valid` is a boolean flag that indicates whether the value is valid or not.

The `UnmarshalJSON` method on the `NullableInt` type implements the `json.Unmarshaler` interface. In this method, we first check if the input data is `null`. If it is, we set `Valid` to `false` and return `nil`. If the input data is not `null`, we use `json.Unmarshal` to parse the integer value into `Value`, and set `Valid` to `true`.

With this custom type in place, you can use it in your struct definition to handle `null` values for integer fields:

```go
type MyStruct struct {
MyInt NullableInt `json:"my_int"`
}
```

Now, when you call `json.Unmarshal` on a JSON string that contains a `null` value for `my_int`, the `MyInt` field in the resulting `MyStruct` instance will have `Valid` set to `false`.
Terminl
2023-09-20 22:38:08 +08:00
我明白你想说什么,当价格返回 null 的时候 go 默认返回 0 。这个问题是属于信息丢失,不好判断是价格 0 元还是商品价格信息丢失,但是只从商品价格来判断商品状态本身就是错误,能够买 0 元商品更是错上加错了。这说明效验商品数据不严谨也太草率
lsry
2023-09-20 22:57:20 +08:00
我觉得你定义的数据类型可能有问题。定义成普通类型,比如 int 的话,应该通过正负来判断合理性。而如果想排除空值 null 或者不传,就应该定义成指针之类的。java 也是类似的方式处理 json 。int 是 0 ,而 Integer 是 null 。
BeautifulSoap
2023-09-21 01:35:52 +08:00
@kiwi95 chatgpt 这个做法我是知道的,但也和上面说全改成指针一样属于目前不太可行的方案。主要几个项目,json 解析相关的 struct 上百个,涉及到字段上千个。没办法简单批量将这些字段全替换成 NullableInt 、NullbaleString 之类的。其次即便想办法批量替换了,所有用到这些字段的代码因为字段类型改变了,全都要重写。对现有项目做如此庞大的更改是真的不太现实(当然新项目可以这么做)

@Terminl 请勿抓住所谓订单、价格不放,随便举个例子罢了,建议你看一下 139L

@ding2dong 你说的很好,没错, null 对应 go 的 nil ,而在 go 中要给 int 类型赋 nil 值是会报错的。所以按照你说的意思,将 nil 解析到 int 字段报错不应该是最符合直觉的行为吗?
BeautifulSoap
2023-09-21 01:56:28 +08:00
@gogogo1203 emmmm 。。。。你怎么就这么被我说说就破防了。。。。莫非真被我说中了。。。。。

我连续两天对线不正好证明我工作量不饱和么。如果我信了 ls 那几位把所有字段都改成指针的提案,我怕不是要多加班一个月哦,哪有这闲心在这和别人对线。
“别人用 go 写 curd 都写了多久了” 不要觉得没人遇到过这类问题,仔细看看这个帖子,已经有很多人提到过遇到过这问题了,以及为了避免这个问题他们用了什么方法了,他们才是真的用 go 写业务有经验的。而这个帖子下回复的很多人(包括你),从回复来看,我深刻怀疑充其量就是用 go 写过小工具,或者只写过规模非常小的项目。用 Go 写实际项目和业务经验之少令人汗颜

其他不说,已经快 200 楼了,就在你上一次回复我的这短短几层之间,又有一位爷跳出来一副高高在上教育别人的口吻在说这是要加 validation 来解决的问题。请问这么多人真的有哪怕动过脑子想过这个问题么?
BeautifulSoap
2023-09-21 02:00:37 +08:00
@snylonue 是不是难以理解?我作为发帖者看了这么多回复,也难以理解为什么那么多人不厌其烦地说是验证问题。我只能说,这个帖子下说是验证问题的,基本全都是想都没想过纸上谈兵的人(而且往往这么说的人口气真的令人不适,所以才一个个怼过去)
Terminl
2023-09-21 02:31:24 +08:00
@BeautifulSoap 因为你设置的是 int ,int 里面就没有 null 。go 处理没有问题,如果要处理 null ,你要自定义一个类型,让 go 判断出来是 null 返回 nil 。
Terminl
2023-09-21 02:40:56 +08:00
我思考了一下,既然设置 int ,不是整数应该要返回 nil 。而不是返回 0
Terminl
2023-09-21 02:48:42 +08:00
我赞同楼主说法,go 的处理不太合理。不应该返回 0 。
snowlyg
2023-09-21 08:51:39 +08:00
正常的开发人员难道不是: 遇到问题,然后查看源码,给出自己的解决方案,然后提交 golang issues .
snowlyg
2023-09-21 08:53:04 +08:00
等到方案被采纳了,再发帖分享一下。
现在在这里发帖和普通网友对线有什么意义?

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

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

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

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

© 2021 V2EX