V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
The Go Programming Language
http://golang.org/
Go Playground
Go Projects
Revel Web Framework
BeautifulSoap
V2EX  ›  Go 编程语言

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

  •  2
     
  •   BeautifulSoap · 2023-09-19 15:28:01 +08:00 · 13801 次点击
    这是一个创建于 435 天前的主题,其中的信息可能已经有所发展或是发生改变。

    精准踩中了 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 包没法简单做到)
    第 1 条附言  ·  2023-09-19 20:34:51 +08:00
    不太理解为什么为什么把 null 解析为默认空值这么严重的问题大家工作似乎都没遇到过。经过这次生产环境事故我认为这是绝对不可接受的。

    随便举个例子,有个 price 字段,类型为 int ,API 接口定义中是非 null 字段。但是请求外部 api 获得返回值 or 前端发送的数据不知为什么获取到了 {"price" : null} 。因为 json 默认把 null 解析为空值,所以解析 json 的时候并不会报错,商品价格会以 0 元被解析(请注意,int 字段为 0 是业务中非常常见的,比如商品价格 0 元是允许的)。那么这就出现了一次非常严重的事故,可能顾客直接以 0 元购买了不应该被购买的商品

    将 null 解析到非指针字段的时候不报错我认为是很严重的问题
    第 2 条附言  ·  2023-09-20 00:20:08 +08:00
    重新整理下,可能是我说明不太好懂,并且很多人用 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 解析不会报错。


    2. 再假设解析的 json 文本如下呢
    `{}`
    答案:解析不会报错,解析后的结果为 `{A:"" B:0 Obj:{AA: "" BB:0}}`

    3. 再假设如下呢
    `{"a":"jack"}`
    答案:解析不会报错,a 之外都赋予默认空值 `{A:"jack" B:0 Obj:{AA: BB:0}}`

    于是这里就有两个坑
    1. json 里的 null 解析到 非 指 针 字段并不会报错,而是直接解析成对应类型的空值。这会造成非常大的问题,因为 {"a": null } {"a": 0} 都会被解析成数字 0 。假设你跟前端/外部接口约定好字段某些字段不能传 null 值,但对方就是因为 bug 传了个 null 值过来,还成功解析成 0 了请问你该怎么办?(在实际业务中数字 0 是非常常见的正常值,如果将 null 解析为 0 后直接用到业务里会出现非常严重的问题,如价格为 0 ,下单数量为 0 之类的)。也许你会说字段全部都定义成指针不就行了?是的,指针可以判定是不是 null ,但我作为负责的后端就要以所有字段都可能被瞎传入 null 为前提考虑问题,所以对于一个复杂业务的复杂 DTO 就会出现下面这样的地狱情况,指针满天飞一不留神就出 BUG 了( PS:Calculate()已经有两个 BUG 了,data.Obj1.I 和 data.I 是可 null 字段,不能直接取值,必须先判空)
    2. json 里不存在的字段解析也不会报错。这点就是上面例子中 2 和 3 。目前业务中还没有迫切需要判别 2 和 3 的需求,但是如果将来遇到的话也将会是非常大的问题
    第 3 条附言  ·  2023-09-20 00:22:49 +08:00
    漏了上面说的指针满天飞的代码链接了 https://gist.github.com/WonderfulSoap/18a14da135f659d5350f36bdbe439b6a
    211 条回复    2023-10-11 17:21:42 +08:00
    1  2  3  
    lanlanye
        101
    lanlanye  
       2023-09-20 10:12:52 +08:00 via iPhone   ❤️ 1
    Go 默认不使用指针而是零值的原因大概和 protocol buffer 一样。
    按照我的理解,业务上要求你区分空值和零值,这个判断就应该你自己来做。基本类型想要精确捕获空值的话就是要么定义成指针,要么参考 Go 的 sql.NullTime 之类的结构实现类似的东西。

    或者,换一种语言会舒服很多。
    leonshaw
        102
    leonshaw  
       2023-09-20 10:21:21 +08:00
    看了一下 op 的 append ,有一个误区。并不是 null 解析成了零值,而是 encoding/json 不区分 null 和 undefined ,传 null 不会改变原来的值。
    调用方传 null 和不传是一样的,如果允许不传,就一定有一个默认值( 0 或非 0 ),那就在 Unmarshal 之前设置默认值;如果不允许不传,那就用其它方法来校验。
    InkStone
        103
    InkStone  
       2023-09-20 10:23:53 +08:00   ❤️ 3
    @Jammar 坑的地方在于这种解析的语义是错误的。如果你这么多年一直都是这么解析的,说明一直都是错误的。

    把 json::null 和 json::int::0 映射都映射到 go::int::0 ,不管从什么角度看都不可能是正确的行为。
    aababc
        104
    aababc  
       2023-09-20 10:29:43 +08:00
    @InkStone goer:你不懂 go 理念的先进之处🐶
    pkoukk
        105
    pkoukk  
       2023-09-20 10:36:19 +08:00
    @BeautifulSoap
    为什么年龄会有 null 呢?如果是必填数据,人还没出生这个人的数据从哪来的?
    如果这是个非必填数据,使用*int 又有什么问题呢?
    感觉很多写 go 的人非常排斥使用*string 或者*int ,想不通为什么
    InkStone
        106
    InkStone  
       2023-09-20 10:41:12 +08:00   ❤️ 1
    @pkoukk 任何语义上不可能有 null 文档也规定了不可以有 null 的字段,在实际传过来的时候都可能有 null 。这对后端来说应该是显而易见的事情……
    Nugine0
        107
    Nugine0  
       2023-09-20 10:42:46 +08:00   ❤️ 1
    @rekulas #38 关 rust 什么事……rust 的 serde_json 库就是 op 想要的效果
    qinyui
        108
    qinyui  
       2023-09-20 10:44:09 +08:00
    ```go
    var a Data

    a.B = -1
    a.Obj.BB = -1

    data := `{"a": "a"}`
    _ = json.Unmarshal([]byte(data), &a)
    fmt.Println(a.A)
    fmt.Println(a.B)
    fmt.Println(a.Obj.AA)
    fmt.Println(a.Obj.BB)
    ```
    在解析前将 int 字段的默认值修改为-1 实现 null 的效果用来跟 0 区分,业务判断等于-1 说明字段没传,算不算一种思路?
    guanzhangzhang
        109
    guanzhangzhang  
       2023-09-20 10:49:41 +08:00
    零值这个没办法,go 就是这样,json 和 yaml 里 null 就是空,go 里就变成零值了
    pkoukk
        110
    pkoukk  
       2023-09-20 10:50:22 +08:00
    @InkStone
    这还是需要区分讨论的,如果我是个内部的 grpc 服务/消息队列消费者,我不会对这种奇怪的情况做兼容
    如果我是个面向外部的 HTTP 服务,我不可能直接用 decode http body 的 struct ,让它透传到数据库或消息队列
    楼主的问题我觉得很好理解,他接受请求用的 struct 用在了后面的很多处理流程里,没有一个“消毒区”,把前端进来的数据处理成面向系统内部流程的 struct ,导致他出现了这么多痛苦的情况
    如果本身接收外部请求的是一个专用 struct ,里面可以包含很多{A *int}这类为了检验而存在的妥协,校验完成后新组成的对象就完全可以是 {A int}了啊
    p1gd0g
        111
    p1gd0g  
       2023-09-20 10:55:47 +08:00
    遇到过,但是当时没觉得是个问题
    kkbblzq
        112
    kkbblzq  
       2023-09-20 10:57:42 +08:00
    这其实无关 json 解析,非指针的默认值的逻辑充斥着 go 的每个地方,你可以说不好,但是的确就是特性;如果你想在解析的时候直接报错,你可以自定义个 NotNullInt ,实现一下 UnmarshalJSON ;至于你说的指针满天飞,实际情况下真有那么多字段需要区分吗?如果这些字段真这么严格的话,处理一下 npe 很难吗?
    webluoye
        113
    webluoye  
       2023-09-20 11:00:29 +08:00
    我们是自己写了一个解析的,里面加了 tag 来判断要执行什么操作。
    ruoge3s
        114
    ruoge3s  
       2023-09-20 11:02:37 +08:00
    换个更喜欢的语言用吧~
    pkoukk
        115
    pkoukk  
       2023-09-20 11:07:07 +08:00   ❤️ 2
    如果你接收到的数据有很大程度的不确定性,那么你做的第一件事应该是消除掉这个不确定性
    我理解你希望 json 包帮你解决这个问题,但它没有不是因为它有问题,而是它要服从于 go 的设计逻辑
    在 go 里,明确有*int 这样的指针来判断这个参数的 “有”和“无”,所以 json 包没必要越俎代庖,否则也会有另一批人跳出来说它有毛病
    你现在的问题很简单,用*int 能解决你的校验问题,但你不愿意用,因为对后续其它逻辑处理流程太麻烦
    我们的解决方案很简单,你只要多加一层 struct 就行了,这层 struct 和你现在 struct 的区别在于,那些不能接受默认值的字段,改成指针
    用这个校验 struct 对进来的数据进行校验,校验通过复制到逻辑 struct 里去,后面的流程不变
    realJamespond
        116
    realJamespond  
       2023-09-20 11:08:27 +08:00
    不用指针就表示要分配 struct 实际内存空间,这个懂一点 c 就知道的,如果是 c 还要手动 memset 成 0 ,现在 go 已经帮你置 0 分配置的内存了还不行么
    MoYi123
        117
    MoYi123  
       2023-09-20 11:18:31 +08:00
    https://gist.github.com/WonderfulSoap/18a14da135f659d5350f36bdbe439b6a

    可能是 null 的用指针, 不可能的用值, 全用指针干什么?
    就算用 optional, 也要判断 has_value()啊, 指针也就多一次寻址, 性能差一点, 代码写起来又差不多的.
    AItsuki
        118
    AItsuki  
       2023-09-20 11:25:56 +08:00 via iPhone
    我觉得大家审题有问题,楼主是想要一种严格的 json 解析方式,非空类型遇到空值应该报错。例如 java 的 gson 就支持。
    我现在的想法是能不能使用自定义 json 来处理,某个类型的字节数组为空就报错,有没有人尝试一下。
    yidadaa
        119
    yidadaa  
       2023-09-20 11:37:18 +08:00
    你需要区分 decoder 和 validator ,有的库会同时提供两个能力,但有的库只会提供 decoder 能力,你去找个带 validator 的就行了
    gps949
        120
    gps949  
       2023-09-20 11:39:08 +08:00
    omitempty
    Granado
        121
    Granado  
       2023-09-20 11:44:06 +08:00
    楼主是想说原子类型不能传 null 吧,这个不是常规 json 解析工具包本该有的功能吗。
    Hilong
        122
    Hilong  
       2023-09-20 11:46:39 +08:00
    @Jammar #100 别这样,刚看到你这个有点崩不住笑出来了,被老板看到了。我绩效奖金要是扣了你要赔我
    xuxihai
        123
    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 时,你会传什么过去?是不是给个空字符串.
    maggch97
        124
    maggch97  
       2023-09-20 12:25:25 +08:00 via Android
    @pkoukk 因为普遍没维护过大型项目,普遍都是微服务,一个库里面几百几千行代码顶天。以为世界是大道至简,以为那些 infra 也是大道至简写出来的。

    那些维护了十年几十年的项目,里面超过一半的代码是在判断异常。
    troywinter
        125
    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
        126
    qW7bo2FbzbC0  
       2023-09-20 12:46:23 +08:00
    接受一门语言,等于接受它的优点的同时也不得不承受它的缺点
    lujiaxing
        127
    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
        128
    lujiaxing  
       2023-09-20 13:03:45 +08:00
    @kumoocat 其实这种在 C# 里就很好解决. C# 里对于值类型有 Nullable<T> 包装类, 简化为 "-?", 如: Int32? Color? 等.
    如果觉得某个值不应该为 NULL, 要么把对应字段改为 Nullable 类型即可. 这样遇到相应字段不存在/null 的情况下, 反序列化回来的就绝不会是默认值了.

    或者更进一步在对应的 Property 上加 [JsonRequired] Attribute. 反序列化发现空值直接报错.
    sylxjtu
        129
    sylxjtu  
       2023-09-20 13:09:59 +08:00 via Android
    不允许出现未知字段和不允许未设置字段是想要解决什么问题?正常情况允许未知字段和允许未设置字段才是正确做法,假如要往 api 加一个字段,不允许未知字段的情况下调用端和服务端需要在同一时间一起更新,如果是跑在多个实例上那还要求所有实例都在同一时间一起更新,明显是有问题的
    BeautifulSoap
        131
    BeautifulSoap  
    OP
       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
        132
    BeautifulSoap  
    OP
       2023-09-20 13:36:56 +08:00
    @lujiaxing
    @sylxjtu
    内啥,莫纠结未知字段、未设置字段。这个帖子争论的重点还是在 null 的解析上。

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

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

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

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

    你听听,这是什么话。you need go-vlidator package.
    menuvb
        141
    menuvb  
       2023-09-20 14:31:30 +08:00
    可以考虑第三方的解析库提取数据并做 nil 判断,避免执行 Mashal/Unmashal 操作。https://github.com/antchfx/jsonquery
    BeautifulSoap
        142
    BeautifulSoap  
    OP
       2023-09-20 14:32:00 +08:00
    @lolizeppelin 你别说,在踩到 null 这个坑之前可能真的挺多人都注意不到这个问题。而且你说的怎么感觉好反思怪啊。学语言总有没有注意到或记住的细节,出问题或者被人指出来前是很难发现的。约定好非 null 但传了 null ,属于不太常发生的事情,所以才很久没出问题。但墨菲定律就是这么发生了,出了事故了

    @pkoukk 你做人怎么能这么双标呢?自己提出所有字段定义成指针,然后被我指出倒腾指针反人类后就提出让我再多写一层 struct 转换过去,在业务层倒腾 struct 。结果我的项目就是这么做的却反过头来说这是臃肿抽象? ps:我不会写 java 谢谢,我和 java 关系最近的也就是曾经学过一小会 kotlin ,仅此而已。项目分层无关语言是几乎所有复杂点的业务写到后来都会自然而然分的。我很喜欢 Go ,但对于像你这样动不动就让人在自己身上找问题,攻击其他语言的 Go 的小部分拥趸,真的很令人不适
    eijnix
        143
    eijnix  
       2023-09-20 14:37:57 +08:00
    用 protobuf
    powerless
        144
    powerless  
       2023-09-20 14:40:06 +08:00
    @Jammar 花西子牌程序员
    pkoukk
        145
    pkoukk  
       2023-09-20 14:54:28 +08:00   ❤️ 1
    @BeautifulSoap #142 如果你在写 go 之前,认真的看过这个东西,你就会明白我说的是什么意思 https://google.github.io/styleguide/go/guide
    不想和你继续下去了,你不是来寻找解决方案的,你只是来吐槽的,给你说再多你都认为别人在攻击你,实在是没必要和你继续浪费时间了
    yanue
        146
    yanue  
       2023-09-20 14:58:57 +08:00
    要通过值判断,而不是判断 null
    BeautifulSoap
        147
    BeautifulSoap  
    OP
       2023-09-20 15:22:24 +08:00
    @pkoukk so ,这和你搞双标有哪怕任何一点关系吗?
    ps:解决办法 21L 就说了已经找到方向了。json 包魔改了一下加了一句判断工作良好已经用到项目里去了。这贴之所拉这么长,是真的太多人的回复(包括你)过于离谱了。就比如这个帖子里所有说这 null 解析是 validation 问题的,明显就试都没试过就一副很懂得样子教育人是 validation ( json 包这个特性实际上根本就没法做 validation )。再比指针,我 2L 就说过为什么不现实了,你硬是要教育人强行说它现实一样
    o562dsRcFqYl375i
        148
    o562dsRcFqYl375i  
       2023-09-20 15:22:36 +08:00
    @Jammar 有没有好好加班
    tiedan
        149
    tiedan  
       2023-09-20 15:29:51 +08:00
    UnmarshalJSON 或者指针,指针其实没啥大问题,稍微有一些嵌套的结构体,指针就是挺多的。。
    chor02
        150
    chor02  
       2023-09-20 15:38:16 +08:00
    这是 go 基础类型特性,使用 mapstructure 做一次校验吧
    lujiaxing
        151
    lujiaxing  
       2023-09-20 15:50:46 +08:00
    @BeautifulSoap 那我就要问问了, 测试干嘛去了. 上线前不做集成测试的么? 怎么可能出现两边接口对不上的问题?

    而且如果你要是真对自己团队的成员都如此不信任的话, 那我就只能建议你放弃 json, 改用更加严格的 SOAP 或者 GRPC 了. 那玩意不用校验, 说好的字段缺一个字都会炸.
    lujiaxing
        152
    lujiaxing  
       2023-09-20 15:55:29 +08:00
    @BeautifulSoap 除非你们其中某一个微服务子系统是用弱类型语言开发的 (Javascript), 否则不可能出现 "某一种情况下有这个字段另一种情况下没有这种字段" 的离谱情况. 如果缺字段, 应该是在测试阶段就已经发现了. 毕竟缺字段会导致整个程序跑不通. 我不明白你们公司的业务是怎么搭建的, 怎么测试的, 怎么可能出现其中一个微服务没有按照约定返回字段却顺利通过了的 codereview, ut, 集成测试的.....
    lujiaxing
        153
    lujiaxing  
       2023-09-20 15:57:12 +08:00
    @lujiaxing 再不就是系统里不当使用了 dynamic 类型
    ttvast
        154
    ttvast  
       2023-09-20 16:16:41 +08:00
    什么叫“默认空值”, 默认值的意思就是你不输入,我就设置成这个值。你给的是 null ,就表示没有输入,所以就是调用默认值。

    对于一个有默认空值的整数字段来说,null 和 0 就是一回事情,设计业务的时候就应该这样定义。
    codersdp1
        155
    codersdp1  
       2023-09-20 16:19:04 +08:00
    竟然声明了 price 为 int ,如果传了{price:null},只有两种结果,1. UnmarshalJSON 不报错,price 为初始值,2.报错
    go 标准库用了第一种 doc 也给出了提示,如果想得到第二种效果得改下标准库满足你的需求,或者定义成*int 。
    BeautifulSoap
        156
    BeautifulSoap  
    OP
       2023-09-20 16:22:58 +08:00   ❤️ 1
    @lujiaxing 别逼逼
    1. "请问你能教教我 json 这种 null 解析逻辑我该怎么做合法性校验呢" 静听高论,希望你能提出切实有效的方案而不是在这纸上谈兵。这个问题我自己已经解决了,只是像听听你会有什么更好的解决办法
    2. 冷知识,实际项目中,外部接口可不光团队内部接口,还可能包括很多质量参差不齐对接的第三方非本公司服务的接口哦
    3. 你是这个帖子第三个还是第四个动不动就问”测试干嘛去了”的人了。现实世界中测试不可能覆盖掉所有情况,总有纰漏,即便花费大心思去思考各种边界条件总归会出纰漏,对于复杂业务你想破脑袋现实都会给你一巴掌。而一有纰漏就质问“测试干嘛去了”的人,我觉得嘴脸十分丑恶
    4. 我只问你一个问题你同不同意“后端不应相信前端/外部接口传来的数据”这一准则。如果你同意,无论你的团队编程质量多高,你就应该以最坏情况去写代码,你死鸭子嘴硬说永远不可能传 null 不校验那就是你自己都不遵守这一准则,把安全性的一切交给外部接口
    zmcity
        157
    zmcity  
       2023-09-20 16:31:04 +08:00
    golang 就是这样的,如果你还不想写指针,就 map[string]interface{}一把梭好了。
    或者直接 grpc ?
    rekulas
        158
    rekulas  
       2023-09-20 16:48:37 +08:00
    @Nugine0 是的我的失误 rust 官方库是支持校验的
    Gota
        159
    Gota  
       2023-09-20 16:49:00 +08:00
    像这种对请求格式要求比较严格的场景,还是建议找个语言无关的 RPC 框架做通信和校验。不然就算一个服务里处理好了,不同语言服务间调用,或者服务质量参差,还是可能遇到类似的问题。
    gogogo1203
        160
    gogogo1203  
       2023-09-20 17:01:40 +08:00
    @pkoukk 因为一开始就吐槽错了,为了掩盖自己的错, 一直持续对线. 标题误导人, 他只是为了吐槽而已.
    leochenL
        161
    leochenL  
       2023-09-20 17:06:17 +08:00
    @BeautifulSoap 说说咋解决的
    yingyan25
        162
    yingyan25  
       2023-09-20 17:36:45 +08:00
    @lshang 我之前也是碰到 OP 这个问题,采用的也是这种方法
    BeautifulSoap
        163
    BeautifulSoap  
    OP
       2023-09-20 17:38:08 +08:00   ❤️ 1
    @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
        164
    BeautifulSoap  
    OP
       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
        165
    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
        166
    gogogo1203  
       2023-09-20 18:58:16 +08:00
    你接着对线了, 我非常想看看这个帖子能更新到什么时候。最好是能火到 go team 过来跟你道歉。
    ding2dong
        167
    ding2dong  
       2023-09-20 19:43:52 +08:00
    null 就是对应 go 的 nil ,有什么问题吗?你的问题是要加验证,而不是寄希望解析规则为你而改变,反正我认为不是坑
    ClericPy
        168
    ClericPy  
       2023-09-20 22:14:17 +08:00
    非 golang 开发, 犹记得当初刚开始用 GO 解析 JSON 被折腾的太痛苦了.

    当时用了好几年 JMESPath, 忘了这玩意居然支持 go 语言, 走了半天弯路最后换语言了 https://jmespath.org/libraries.html
    snylonue
        169
    snylonue  
       2023-09-20 22:14:28 +08:00   ❤️ 1
    不明白为什么那么多人说是验证的问题,`{ "int_field": 0 }` 和 `{ "int_field": null }` 解析成一个结果明显是丢掉了信息
    kiwi95
        170
    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
        171
    Terminl  
       2023-09-20 22:38:08 +08:00
    我明白你想说什么,当价格返回 null 的时候 go 默认返回 0 。这个问题是属于信息丢失,不好判断是价格 0 元还是商品价格信息丢失,但是只从商品价格来判断商品状态本身就是错误,能够买 0 元商品更是错上加错了。这说明效验商品数据不严谨也太草率
    lsry
        172
    lsry  
       2023-09-20 22:57:20 +08:00
    我觉得你定义的数据类型可能有问题。定义成普通类型,比如 int 的话,应该通过正负来判断合理性。而如果想排除空值 null 或者不传,就应该定义成指针之类的。java 也是类似的方式处理 json 。int 是 0 ,而 Integer 是 null 。
    BeautifulSoap
        173
    BeautifulSoap  
    OP
       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
        174
    BeautifulSoap  
    OP
       2023-09-21 01:56:28 +08:00
    @gogogo1203 emmmm 。。。。你怎么就这么被我说说就破防了。。。。莫非真被我说中了。。。。。

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

    其他不说,已经快 200 楼了,就在你上一次回复我的这短短几层之间,又有一位爷跳出来一副高高在上教育别人的口吻在说这是要加 validation 来解决的问题。请问这么多人真的有哪怕动过脑子想过这个问题么?
    BeautifulSoap
        175
    BeautifulSoap  
    OP
       2023-09-21 02:00:37 +08:00
    @snylonue 是不是难以理解?我作为发帖者看了这么多回复,也难以理解为什么那么多人不厌其烦地说是验证问题。我只能说,这个帖子下说是验证问题的,基本全都是想都没想过纸上谈兵的人(而且往往这么说的人口气真的令人不适,所以才一个个怼过去)
    Terminl
        176
    Terminl  
       2023-09-21 02:31:24 +08:00
    @BeautifulSoap 因为你设置的是 int ,int 里面就没有 null 。go 处理没有问题,如果要处理 null ,你要自定义一个类型,让 go 判断出来是 null 返回 nil 。
    Terminl
        177
    Terminl  
       2023-09-21 02:40:56 +08:00
    我思考了一下,既然设置 int ,不是整数应该要返回 nil 。而不是返回 0
    Terminl
        178
    Terminl  
       2023-09-21 02:48:42 +08:00
    我赞同楼主说法,go 的处理不太合理。不应该返回 0 。
    snowlyg
        179
    snowlyg  
       2023-09-21 08:51:39 +08:00
    正常的开发人员难道不是: 遇到问题,然后查看源码,给出自己的解决方案,然后提交 golang issues .
    snowlyg
        180
    snowlyg  
       2023-09-21 08:53:04 +08:00
    等到方案被采纳了,再发帖分享一下。
    现在在这里发帖和普通网友对线有什么意义?
    ding2dong
        181
    ding2dong  
       2023-09-21 09:00:49 +08:00
    @BeautifulSoap 实际上我早就注意到这个问题了,比如需要局部跟新某些字段,如果不特殊处理就会把其他字段更新为 0 值。这时候用指针类型来解析就是了。但这种情形确实比较少,遇到了就特殊处理。至于说解析到 null 报错,那就离谱了,到时候又说,别的语言可以解析 null ,go 怎么报错 balabala 的

    -------------------------
    @ding2dong 你说的很好,没错, null 对应 go 的 nil ,而在 go 中要给 int 类型赋 nil 值是会报错的。所以按照你说的意思,将 nil 解析到 int 字段报错不应该是最符合直觉的行为吗?
    jsboy
        182
    jsboy  
       2023-09-21 09:45:10 +08:00
    @Terminl 有个点不对,不是 json 解析赋值 0 ,是 null 不处理,导致 go 语言采用了默认值。所以其实不能说 json 解析的问题。对于 json 来说为 null 是不存在的意思。
    -----------------------
    这个问题是其实 java 也有,如果用 int 而不用 Integer ,null 值结果也是 0 ,不同的是 java 中 Integer 的使用和 int 不会有太大的区别,可以自动装箱 拆箱,所以一般都会把字段定义为 Integer 这样的类型。

    个人觉得,go 如果也能像 java 一样在指针类型和普通类型的转换上变得方便,采用指针定义属性类型应该还是正确的解决方式,而且 json 中属性可以为 null ,能和 go 中属性为指针类型对应。也更容易理解。

    另外自己修改了基础库后续的升级更新也会带来麻烦,只是这个麻烦不在现在,或者说你觉得麻烦更小,但没必要说服别人要和你一样使用这样的解决方案。
    kiwi95
        183
    kiwi95  
       2023-09-21 09:56:22 +08:00   ❤️ 2
    @BeautifulSoap #173 如果牵涉面这么广的话,魔改 json 包很可能 break 别的功能吧,基于你的需求,写了一个 toy validator 刚传到 GH ,通过 strcut tag 在`json.Unmarshal` 之前验证: https://github.com/wuxu92/json-validator
    xiaocaiji111
        184
    xiaocaiji111  
       2023-09-21 10:41:00 +08:00
    go 的零值是很恶心有时候,不报错,但是业务逻辑是不对的。有时候分不清是没传值导致的零值的 0 ,还是业务上的 0 。只能用指针解决。
    特别是跟外部交互,比如 http 接口,rpc 调用,操作数据库。
    myderr
        185
    myderr  
       2023-09-21 10:54:56 +08:00
    我测试了一些 c#的 Newtonsoft.Json ,发现:
    1 、如果 json 字符串里面没有 IntA 这个字段,对象里面的 IntA 字段是默认值 0
    2 、如果 json 字符串里面有 IntA 并且是 null ,解析报错 converting value {null} to type 'System.Int32'. Path 'IntA', line 1, position 45.”
    Nugine0
        186
    Nugine0  
       2023-09-21 11:32:22 +08:00
    @snowlyg #180 go 官方不可能采纳的,想想也知道这种标准库行为没法改的吧
    snowlyg
        187
    snowlyg  
       2023-09-21 11:34:12 +08:00
    @Nugine0 所以官方不采纳就自己写一个第三方库咯,然后分享一下自己的库。如果单纯的吐槽真的是没啥意义。
    Nugine0
        188
    Nugine0  
       2023-09-21 11:38:36 +08:00
    @snowlyg #187 有些回复确实离谱,难怪 op 上火
    wwwuser
        189
    wwwuser  
       2023-09-21 11:41:16 +08:00
    看了下官方 Unmarshal 的规则,// To unmarshal JSON into an interface value,
    // Unmarshal stores one of these in the interface value:
    //
    // bool, for JSON booleans
    // float64, for JSON numbers
    // string, for JSON strings
    // []interface{}, for JSON arrays
    // map[string]interface{}, for JSON objects
    // nil for JSON null
    //
    wwwuser
        190
    wwwuser  
       2023-09-21 11:44:41 +08:00
    也就 Unmarshal 到非 interface{} 会产生 null 和 0 分辨不清楚,所以先 Unmarshal 到 interface{}, 做一下校验就 OK 了,没必要改官方包
    wwwuser
        191
    wwwuser  
       2023-09-21 11:50:35 +08:00
    // The JSON null value unmarshals into an interface, map, pointer, or slice
    // by setting that Go value to nil. Because null is often used in JSON to mean
    // “not present,” unmarshaling a JSON null into any other Go type has no effect
    // on the value and produces no error.
    所以先把原始 json 数据想办法 unmarshal 到 interface, map, pointer, or slice 这些里面做个校验就行,改官方包也算是一种方法吧, 只是觉得没必要大动干戈改官方包。
    dqzcwxb
        192
    dqzcwxb  
       2023-09-21 12:02:54 +08:00
    其他语言有问题就是语言问题
    golang 有问题就是人有问题
    aababc
        193
    aababc  
       2023-09-21 12:58:46 +08:00
    @dqzcwxb 老哥,真相了,虽然我也写 go ,但是对这门语言确实很难有认同感
    snowlyg
        194
    snowlyg  
       2023-09-21 14:00:45 +08:00
    @Nugine0 是的,所以我一般都不会自讨没趣。网上是很难找到认同感的。
    meioco3
        195
    meioco3  
       2023-09-21 15:33:07 +08:00   ❤️ 1
    @leoleoasd 把类型设为 *int 就可以区分 {"a": 0} 和 {"a": null } , go 语言的结构体包括变量初始化后是零值很合理。这个不是 json 库的问题,你总不能要求 go 给你将 int 类型的变量或字段储存一个 nil 或 null 吧 ?
    thevita
        196
    thevita  
       2023-09-21 15:40:24 +08:00
    问题还是 go 的 部分 value type 没有 null, 这点与 json 不一致,要弥合这点,

    1. 让 go type 去 match json ,可以引入 Null* 的 type, NullString, NullNumber 什么的,
    2. 让 json match go type, 依然通过 NewType 实现 UnmarshalJSON 来验证数据呗
    codersdp1
        197
    codersdp1  
       2023-09-21 16:14:42 +08:00
    @kiwi95 #183 改动太多,不符合 op 要求。
    codersdp1
        198
    codersdp1  
       2023-09-21 16:20:04 +08:00
    @Terminl #171 自以为会解析报错,结果不报错,那只能给语言背锅咯。
    hxtheone
        199
    hxtheone  
       2023-09-21 16:36:23 +08:00 via iPhone   ❤️ 1
    翻下来感觉最根本的原因还是 go 的值类型有默认的空值并且不能为 nil 也没有 optional 之类的容器, 我见过的 go 新手或多或少都会踩到这个坑, 比如 json null 不解析, gorm 里空值不更新数据库这样的问题. 没办法语言的底层就是这样设计, go 也在不违反这个设计的前提下提供自定义 marshaller/unmarshaller 来实现个性化的需求, 嫌麻烦的话引第三方包或换语言呗
    thevita
        200
    thevita  
       2023-09-21 16:43:30 +08:00
    真要说起来,
    @hxtheone 是的,就是 type system 不一致的原因,类似的情况其他语言也一样会存在,同样的问题 sql 也一样存在,完全可以参考 sql 的解决方案呗,扯半天扯不到点上。
    1  2  3  
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   3169 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 31ms · UTC 13:49 · PVG 21:49 · LAX 05:49 · JFK 08:49
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.