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

243 天前
 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 包没法简单做到)
12872 次点击
所在节点    Go 编程语言
211 条回复
BeautifulSoap
243 天前
@haoxue 多谢我试试
@tuxz 正常来说一个实际的项目中,要从 json 解析出 struct 的数量是几十上百个的。。。一个个实现不太现实,写通用方法塞进去倒也是个办法,但是也容易出纰漏


@ye4tar
@RedisMasterNode
是的,我思前想后可能也就只能自己魔改官方 json 包了。不过倒也用不着 validator ,直接在解析 json 的时候,如果目标字段为非指针类型,遇到 null 就直接报错可能会更直接的。一直不太想自己魔改主要是总感觉自己写的东西是重复造轮子,,,,
Maboroshii
243 天前
@BeautifulSoap 你也知道,你列举的这些语言,对象是可为 null 的。那你知不知道,golang 里面,struct 只有 0 值没有 null 值呢。
kumoocat
243 天前
@BeautifulSoap Java 直接用基础 int 类型也是 0 啊,Go 不用指针就代表不能为 null
BeautifulSoap
243 天前
@tairan2006 要改成指针的可不止匿名类,匿名类里的 AA 、BB ,外面的 A 和 B 也都得要改成指针哦。然后就出现了我 2L 说的问题,为了解决 null 判定这一个问题,整个 struct 全部字段都定义成指针,实在过于得不偿失了。

@Maboroshii 其实问题不在“已知字段会为 null”,而是 API 文档已经明确约定了些字段全都不能为 null ,但外部接口/前端就硬是给你传了 null (不要问为什么,后端永远不能相信前端传给你的数据是什么牛鬼蛇神,实际上这次出事就是因为约定了非 null 的字段外部接口给传了 null 。)
houshuu
243 天前
前端返回和设计文档上不一样的内容那应该归属于前端导致的问题, 而不是力求后端在脱离设计初衷的情况下按照自己预想的工作方式工作.
如果设计上允许前端有可能传入 null, 那你在后端设计上就应该使用指针, 使用 int 就是个错误的选择.

最后, 这都涉及实际订单处理和付款了, 就算设计不完备, 这个问题也理应在发布前的测试过程中被发现才对.
xiangyuecn
243 天前
任何语言,json 到 map 一把梭
ysc3839
243 天前
@Maboroshii C++对象也是不可为 null 的,但是许多 C++的 JSON 库懂得把 null 对应到 std::optional 或者其他的 optional 实现。Golang 这个问题实际上反映了缺乏类似 std::optional 这样的表达 null 的工具。
BeautifulSoap
243 天前
@Maboroshii
@kumoocat
“Go 不用指针就代表不能为 null”
“那你知不知道,golang 里面,struct 只有 0 值没有 null 值呢”

你们说的对啊,Go 里一个类型不是指针代表这个类型不能赋值 nil ,json 解析的时候遇到 null 意思就是要把 nil 赋值给非指针,直接报错不是再正常不过的想法吗?


“你也知道,你列举的这些语言,对象是可为 null 的”
这里面和 go 最相近的是 Go ,因为 kotlin 和 go 一样类型分可空/非可空(对应到 go 近似看成指针/非指针)。当尝试将 null 解析到非可空字段时,kotlin 是可以报错的
gogogo1203
243 天前
// Decode reads the body of an HTTP request looking for a JSON document. The
// body is decoded into the provided value.
//
// If the provided value is a struct then it is checked for validation tags.
func Decode(r *http.Request, val interface{}) error {
decoder := json.NewDecoder(r.Body)
decoder.DisallowUnknownFields()
if err := decoder.Decode(val); err != nil {
return err
}

return nil
}



type NewComment struct {
Body string `json:"body" validate:"required"`
ParentId string `json:"parentId"`
}




....
var nc comment.NewComment
if err := web.Decode(r, &nc); err != nil {
return fmt.Errorf("unable to decode payload: %w", err)
}

....

很久没有写 go 了,go 不至于连个 json 不能处理吧
delacey
243 天前
@BeautifulSoap 后端肯定需要做校验的,因为调用你接口的不仅仅只有前端,还可能是不怀好意的脚本小子
BeautifulSoap
243 天前
@houshuu 你可能都没看懂我这贴的意思到底是什么。我只是举例,出的事故不是订单相关。

后端不应该信任前端/外部端口传入的数据,所以后端需要对传入的数据做最低限度的确认,如果不符合预期和要求就要报错。请问这点你同不同意?
这次问题就出在“对传入数据做最低限度的确认”上,API 接口约定不能传 null ,但是外部接口传了 null ,在 json 解析的时候自然而然会认为要给非指针赋值 nil ,json 包肯定会报错,但实际上它解析成空值不报错,而空值 0 是业务允许的,从而引发了生产事故

自然,传了 null 的确是前端/外部接口的锅,但除了生产事故加班 log 调查、数据查找、确立回复数据库数据都是后端/运维的工作。到时候如果来一句“虽然 xxx ,但后端连 null 都不检查的吗”就问你怎么应对。
BeautifulSoap
243 天前
@delacey 我顶楼也说了,直到踩到坑之前我都一直以为 json 包替我完成了对 null 的校验啊。你要想,json 包做解析的时候吧 string 解析到 int 类型字段会报错,那么谁会想到把 null 解析到非指针字段就不会报错了呢?

> 都是不出事就注意不到的问题。尤其非指针类型字段,我下意识认为遇到 null 是会直接报错的,结果直接是当作不存在(undefined)来处理。。。
gogogo1203
243 天前
@delacey 我看过比较好的方式是 前端来的 json 用一个 struct, 校验完转成 db 专用的另一个 struct, db 查询返回的又用另外一个新的 struct. 前端更新某个 field , 又用一个专属的 struct. 用 go 写 curd 后台,做得最多的就是各个 struct 之间转来转去。
BeautifulSoap
243 天前
@gogogo1203 这方法使用 DisallowUnknownFields 能解决问题 1 ,但问题 2 没办法解决
gogogo1203
243 天前
@BeautifulSoap

```
// Create adds a Comment to the database. It returns the created Comment with
// fields like ID and DateCreated populated.
func (c Core) Create(ctx context.Context, nc NewComment, now time.Time, usrId string, tutId string) (Comment, error) {
if err := validate.Check(nc); err != nil {
return Comment{}, fmt.Errorf("validating data: %w", err)
}
parentId := &dbschema.NullString{}
if nc.ParentId == "" {
parentId.Valid = false
} else {
parentId.Valid = true
parentId.String = nc.ParentId
}
dbCm := db.Comment{
ID: validate.GenerateID(),
CommenterId: usrId,
TutorialId: tutId,
ParentId: parentId,
Body: nc.Body,
DateCreated: now,
DateUpdated: now,
}
if err := c.store.Create(ctx, dbCm); err != nil {
return Comment{}, fmt.Errorf("create: %w", err)
}

return toComment(dbCm), nil
}

```

web 到业务逻辑 然后再到 db 层,json tag 校验是必备的。sql.NullString sql.NullInt 都不是简单的 struct ,理论上不存在 go 后端不查 null 的,因为必须确认. 你们是不是用了什么 orm 一把梭了.
BeautifulSoap
243 天前
@gogogo1203 #33 “前端来的 json 用一个 struct, 校验完” 其实问题就出现在怎么校验这一步上啊。null 会被解析成对应字段类型的空值,也意味着根本没有手段去校验 json 传入的是不是 null 。问题就出现在这一步了。我顶楼给的那几个涉及到 null 的例子就是最好写照
Yourshell
243 天前
rekulas
243 天前
@BeautifulSoap 拿 php python 这种动态脚本语言来类比 go,很让你怀疑你的基础水平
kotlin 还差不多
但是, 你说的 kotlin 报错并不代表所有语言都要按照你喜好的语言来走, 严格来说,你的需求就是 2 步: 1 解析 2 验证非法值
如果 kotlin 直接在 json 解析阶段做了校验,我认为没有问题, 但是因为它做了就 diss 别的语言就显得太幼稚了, 如果你要这样来的话,麻烦你先把 c 和 rust 先 diss 一遍,按我经验,一般流行的库在解析阶段也不会报错,除非加了数值校验
再举个例子,以后再出一款高级语言,在 json 解析的时候支持自动反序列化,数值严重,enum 验证,time 类型验证...那新语言的开发者是不是又继续来鄙视这些老前辈呢?

如果你非要区分 0 和 null,按楼上说的用指针才是正道,你用了错误的方法去处理问题还一直想不通,感觉有点钻牛角尖了
lshang
243 天前
多说一句,除了 null 以外,如果某个字段没填,json unmarshal 到非指针字段给的也是零值,也是不好区分的。
可能的一种做法是你封装一层 json unmarshal ,先通过反射把结构里必填的字段获取个列表,然后把数据先 unmarshal 到 map ,做是否填写的校验。
通过之后再 unmarshal 到对应结构上。
deorth
243 天前
op 说得对,golang 的官方 json 就是一坨

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

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

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

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

© 2021 V2EX