「开发日志」踩到 Go 的 json 解析坑了,如何才能严格解析 json?插图

引言

最近在使用 Go 进行 json 解析时,遇到了一些问题导致生产环境出错。这篇博客将分享我踩到的两个 json 解析包的坑,并介绍如何进行严格的 json 解析

「开发日志」踩到 Go 的 json 解析坑了,如何才能严格解析 json?插图1
GoLang

问题背景

首先,我们假设有以下结构体定义:

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 数据会报错?

答:所有的 json 数据都可以正确解析,不会报错。

解析问题分析

这些问题都是很容易忽略的细节。特别是对于非指针类型的字段,我本能地认为遇到 null 应该会直接报错,但实际上 Go 的 json 解析会将其当作不存在(undefined)来处理。

那么,我们如何才能简单地进行严格的 json 解析呢?我们的要求是:

  1. 不允许出现未知字段,如果出现则报错(可以使用 json 包的 DisallowUnknownFields 实现);
  2. 非指针类型字段不允许传入 null,否则报错(似乎 json 包无法简单实现)。
「开发日志」踩到 Go 的 json 解析坑了,如何才能严格解析 json?插图2

为什么严格解析很重要?

你可能会好奇为什么将 null 解析为默认空值这个问题那么严重,大家的工作似乎都没有遇到过。通过生产环境中的事故,我认为这绝对是不可接受的。

举个例子,假设有一个名为 price 的 int 类型字段,在 API 接口定义中是非空字段。但是当请求外部 API 或前端发送数据时,却不知道为什么获取到了 {"price" : null} 这样的返回值。由于 json 默认将 null 解析为空值,所以在解析 json 数据时并不会报错,商品价格就会被解析成零元(请注意,0 在业务中是非常常见的,比如商品价格为零是允许的)。这将导致严重的事故,可能导致顾客购买了本不应该购买的商品。

将 null 解析到非指针类型字段并不报错,我认为这是一个非常严重的问题

解决方案

重新整理一下,可能是我之前表述不够清晰,很多人在使用 Go 的 json 解析时并不关注细节,所以对我的意图没有理解。

例如,我定义了一个名为 Data 的结构体,然后使用以下方法解析 json:err := json.Unmarshal([]byte(jsonStr), &data)

  1. 假设要解析的 json 文本如下,请问:会报错吗?如果不报错,解析的结果是什么?
{"a":null, "b": null, "obj":null}

答案:不会报错,解析后的结果为 {A:"", B:0, Obj:{AA:"", BB:0}}。null 直接被解析为各个类型的默认空值,而不是报错。通常情况下,在 Go 中与 null 最接近的概念是 nil。将 null 解析到非指针类型相当于将 nil 赋值给字段 A、B 和 Obj。如果将报错视为理所当然的事情,那么在实际上 Go 的 json 解析不会报错。

  1. 再假设解析的 json 文本如下:
{}

答案:不会报错,解析后的结果为 {A:"", B:0, Obj:{AA:"", BB:0}}

  1. 再假设解析的 json 文本如下:
{"a":"jack"}

答案:不会报错,除了字段 a 被赋予了值之外,其他字段都被解析为默认空值,即 {A:"jack", B:0, Obj:{AA:"", BB:0}}

「开发日志」踩到 Go 的 json 解析坑了,如何才能严格解析 json?插图3

总结起来,这里存在两个坑:

  1. 将 json 中的 null 解析到非指针类型字段时,并不会报错,而是直接解析为空值。这会导致非常大的问题,因为 {"a": null} 和 {"a": 0} 都会被解析成数字 0。假设你与前端或外部接口约定某些字段不能传入 null 值,但对方却因为 bug 而传入了 null 值,然后成功解析为 0,你该如何处理呢?(在实际业务中,数字 0 是非常常见的正常值,比如价格为 0 或者购买数量为 0)。也许你会说将所有字段定义为指针类型,但是作为后端的我们需要考虑所有字段都可能传入 null 的情况。对于一个复杂的业务 DTO,就会出现指针满天飞的情况,一不留神就会出现 BUG(比如 Calculate() 方法已经有两个 BUG 了,data.Obj1.I 和 data.I 是可空字段,不能直接取值,必须先判空)。
  2. json 中不存在的字段也不会报错。这点可以参考例子中的第 2 和第 3 种情况。目前业务中并没有迫切需要判断这两种情况,但如果将来需要的话,问题会变得非常严重。

结尾

我们应该在进行 json 解析时格外小心,尤其是涉及到 null 值的处理。建议在代码中添加额外的校验和处理逻辑,以确保解析的准确性和安全性。