踩到 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 包没法简单做到)
15402 次点击
所在节点    Go 编程语言
211 条回复
Granado
2023-09-20 11:44:06 +08:00
楼主是想说原子类型不能传 null 吧,这个不是常规 json 解析工具包本该有的功能吗。
Hilong
2023-09-20 11:46:39 +08:00
@Jammar #100 别这样,刚看到你这个有点崩不住笑出来了,被老板看到了。我绩效奖金要是扣了你要赔我
xuxihai
2023-09-20 12:17:01 +08:00
JSON 只是一个序列化和反序列化的方案,必然是一开始就约定好了两端的数据格式,既然约定了 a 为 int 类型,传 {"a": null } 当成 {"a": 0} 有什么问题呢,换句话就是,"我们约定好了,这个地方,我要一个数字哦,你传其他的我都不认(反序列化错误),你传 null?, 这么极端的吗? 那行,那个 null 和 0 没啥区别", 如果觉得 a 为 0 不合适,校验阶段处理就好。不应该在反序化的过程处理所有的情况,还不能理解,自己实现一个序列化和反序列化的方案,按二进制的来, {"a": null }中,如果 a 后台当成 int ,你会传什么过去? 是不是会给个 0 传过去,当成 string 时,你会传什么过去?是不是给个空字符串.
penzi
2023-09-20 12:25:25 +08:00
@pkoukk 因为普遍没维护过大型项目,普遍都是微服务,一个库里面几百几千行代码顶天。以为世界是大道至简,以为那些 infra 也是大道至简写出来的。

那些维护了十年几十年的项目,里面超过一半的代码是在判断异常。
troywinter
2023-09-20 12:28:09 +08:00
这问题和序列化没有太大关系,go 的 struct 初始化机制就是这样了,这种问题应该在接触 go 的第一天就清楚,很多教程里也写的明明白白,对于 json 的问题,文档也写的明明白白,要么 omitempty 要么看 json 包的注释:
"Pointer values encode as the value pointed to. A nil pointer encodes as the null JSON object."
qW7bo2FbzbC0
2023-09-20 12:46:23 +08:00
接受一门语言,等于接受它的优点的同时也不得不承受它的缺点
lujiaxing
2023-09-20 12:59:42 +08:00
搞什么鬼? JSON 本来就是非严格结构好不好? 经常会有 A 跟 C 对接, A 提供 a, b, c, d 字段但是 C 接受 b, c, e, f 的情况. 毕竟 C 可能还要接 B. 所以遇到 "C 接 A 的时候 e, f 没有值, 接 B 时候 b 没有值" 是极其正常的情况.

如果你觉得这种出现默认值的情况不合理, 要么要求调用方修改, 要么自己在代码里做 Guarding.

要么就不要用 JSON 改用 SOAP XML.
lujiaxing
2023-09-20 13:03:45 +08:00
@kumoocat 其实这种在 C# 里就很好解决. C# 里对于值类型有 Nullable<T> 包装类, 简化为 "-?", 如: Int32? Color? 等.
如果觉得某个值不应该为 NULL, 要么把对应字段改为 Nullable 类型即可. 这样遇到相应字段不存在/null 的情况下, 反序列化回来的就绝不会是默认值了.

或者更进一步在对应的 Property 上加 [JsonRequired] Attribute. 反序列化发现空值直接报错.
sylxjtu
2023-09-20 13:09:59 +08:00
不允许出现未知字段和不允许未设置字段是想要解决什么问题?正常情况允许未知字段和允许未设置字段才是正确做法,假如要往 api 加一个字段,不允许未知字段的情况下调用端和服务端需要在同一时间一起更新,如果是跑在多个实例上那还要求所有实例都在同一时间一起更新,明显是有问题的
sylxjtu
2023-09-20 13:13:12 +08:00
BeautifulSoap
2023-09-20 13:17:56 +08:00
@pkoukk 我觉得包括你在内,很多人都有个问题,问的时候都一个个口口声声说后端不能信任前端/外部接口传来的数据,实际真的干起活来怎么就觉得不用遵循这一基本要求了呢? API 文档约定是一回事,实际过来什么数据只有天知道。作为后端肯定要以所有字段都可能被瞎传入 null 为前提考虑问题,遇到约定不能传 null 的字段传 null 了就报错,这难道不是后端的基本素养和常识?

其次,不好意思,作为用 Go 参与了 N 个复杂业务项目的表示,请求从 DTO 进来、转成 Entity 处理业务逻辑,最后在 repository 层里转成 model/DO 是我写项目的基本要求。但问题来了,json 解析不光出现在 DTO ,我请求外部接口也要解析 josn 哦。你说的全部定义成指针,那么这意味我一切 API 的一切 DTO 字段,所有请求外部接口解析结果获得的字段都要定义成指针,当然转成 entity 后业务层是不用太在乎,但我只能说,哦,真的太优雅了,工作量真的是太不饱和了。而且,即便是 DTO 层和外部接口请求也多少会有点和业务无关的逻辑的,让你对这些指针倒来倒去是真的不切实际的提案。哦,你说在 DTO 和 Entity 之间在加一层 struct 专门用来处理 DTO 层的部分逻辑?哦,见到这样写的代码大概率我会拒绝 review 的,因为照你这样搞这个项目 DTO 层漫天飞舞的指针就是今后屎山的起点
BeautifulSoap
2023-09-20 13:36:56 +08:00
@lujiaxing
@sylxjtu
内啥,莫纠结未知字段、未设置字段。这个帖子争论的重点还是在 null 的解析上。

@maggch97 说得很对,实际做了这么多项目后的感叹就是,实际复杂点的业务项目要考虑的问题太多了。光是一个外部交互检查有问题,或者业务逻辑层考虑不周全就很可能导致后面业务流程出大问题。虽然业务代码已经进最大努力将所有能想到的情况都检查了,但实际项目运行中依旧会时不时出现一个完全意想不到 case 导致出问题。
zlstone
2023-09-20 13:38:16 +08:00
如果 go 也有类似 rust serde 的库就好了,直接校验、解析全搞定
peefy
2023-09-20 13:42:05 +08:00
可以考虑对数据预先进行校验 JSON -> 某种校验规则/库/DSL -> Go Struct, 比如 JsonSchema 什么的
lujiaxing
2023-09-20 13:50:51 +08:00
@BeautifulSoap 对于值类型 (或称原子类型) JSON 字段的 NULL / Undefined 的解析就应该解析为类型默认值. 即: 0.
这是这么多年约定俗成的要求, 也是标准做法. 没什么可争论的. 说了好几遍了 "后端不能信任前端/外部接口传来的数据". 你自己不做数据合法性校验难道还赖前端不给你传么?? 没人说你一定要把 DTO 里改成指针, 你可以不改啊, 但是你代码逻辑肯定要判断啊, 尤其这种支付的场景, 前端本就不该把金额传给你好不好? 标准做法应该是只给你传一个订单号, 以及用户在支付平台扫码得到的 AuthToken... 金额/SKU 摘要/标题等是要你从订单里翻出来的!! 哪儿有前端告诉你是多少钱就是多少钱的道理?
lolizeppelin
2023-09-20 13:53:45 +08:00
好奇你写了多久 GO,稍微写一段时间就知道 go 没法区别普通空字段和 null 字段了把
这是 go 语言本身的问题,go 语言傻逼处挺多的,但你居然会因为这个问题在生产环境踩坑

你应该反思下自己 2333 喷 go 没用...解决不,解决方式也很别扭,受不了换语言呀....
Nugine0
2023-09-20 14:00:57 +08:00
@Jammar #100 李佳琦直呼内行
pkoukk
2023-09-20 14:04:53 +08:00
@BeautifulSoap 你怎么写个 golang 一股 java 味?看着这些臃肿的抽象层我就头疼,如果你执意要这么玩,那最好还是用回 java 吧,golang 不适合这个玩法
BeautifulSoap
2023-09-20 14:07:24 +08:00
@lujiaxing
1. 你和 ls 几位一样,我只是随便举个价格的例子而已你就只抓表象问题说事,遇到的业务问题不是价格也不是订单,只是订单更加容易理解
2. 我从来说的都是前端/外部接口解析的问题,请注意后者“外部接口”。你真的以为“金额/SKU 摘要/标题等是要你从订单里翻出来的!!”不会遇到 null 解析问题?假设你有订单 id ,然后订单详情需要请求另一个微服务 API 获取,这个 API 以 json 形式返回订单详情。好了,问题来了,这个微服务出了 bug 原本约定好禁止返回 null 的订单的金额、数量返回了 null 。请问你如何应对?请问你不还是遇到了同样的问题?这个问题不光是前段的,只要涉及到外部接口和 json 就会遇到

还有,请问你能教教我 json 这种 null 解析逻辑我该怎么做合法性校验呢?
uiosun
2023-09-20 14:23:17 +08:00
有没有可能,JSON 支持 null……换句话形容你的情况:

JSON 正常解析,我该怎么让让它不正常解析?

你听听,这是什么话。you need go-vlidator package.

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

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

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

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

© 2021 V2EX