踩到 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 包没法简单做到)
13958 次点击
所在节点    Go 编程语言
211 条回复
gogogo1203
2023-09-20 01:01:52 +08:00
@BeautifulSoap 2023 年了,喜欢就用,不喜欢就换个语言,没必要抓个标准库输出。多一次运算累计起来的数量都是以亿计算的,我宁可相信 go team 是考虑过这些事的。
gogogo1203
2023-09-20 01:07:23 +08:00
@leoleoasd 这有什么区别吗? struct 创建的时候就是默认零值,unmarshal 只是选择性宽容。你后续按自己业务需求去校验。一群人在喷一些什么东西。又不是所有场景都需要判断。
leoleoasd
2023-09-20 04:29:33 +08:00
@gogogo1203 #62 你觉得校验库怎么区分 {"a": 0} 和 {"a": null } ?
Nasei
2023-09-20 07:44:32 +08:00
在默认情况下,把 json 的 null 对应的 struct 的零值,我觉得没问题

只不过官方库可能没提供更全的选项让你选择,这时候用第三库就可以了
rekulas
2023-09-20 08:08:12 +08:00
@BeautifulSoap 10 多年开发经验了 确实没遇到过你的问题
就以你上面说的 php 为例, 加入 php 做后端,计算前端传入{"a": null}给你,你又能如何呢, 解析同样正常, 你仍然需要在业务层做数据校验,你要解析时报错,除非使用支持的第三方库

当我们需要判断一个值的时候当然可以 if a> 0 ...
当我们需要判断 100 个值得时候, 正常人都会选择复用方法来实现, 你还要按照一个值的方法来使用还指责语言不够完善就不合理了,而且我上面也给你提到了好用的三方库,也忽视不见
要按这样说,c 语言官方连 json 支持几乎为 0 呢,那不是更应该吊起来打
k9982874
2023-09-20 08:20:04 +08:00
典型的我不要你觉的,我要我觉得。50 楼已经把答案贴上来了。
lovelylain
2023-09-20 08:20:42 +08:00
@BeautifulSoap “要改成指针的可不止匿名类,匿名类里的 AA 、BB ,外面的 A 和 B 也都得要改成指针哦。”
兄弟看来是没用过 proto3 ,这个缺省零值的设定习惯了还是挺好用的,首先大部分零值在业务上本身就对应无效值,或者可以通过取值范围设计对应为无效值,例如 string 如果按你的想法传 null 直接解析时报错,但你程序里往往还要检查非空,所以直接解析时多一步 null 检查就有点多余了,枚举类型的 0 值同理;对于极少数 0 值合法,且你要区分是没传还是传入了 0 值的情况,可以用指针,这种情况显然是非常少的。
tramm
2023-09-20 08:26:50 +08:00
RPC 调用 Java 哈哈哈
kiwi95
2023-09-20 08:30:38 +08:00
这只能说是标准库的一种取舍,对你可能不方便,但是对大部分人可能是一种更可接受的行为。并且标准库文档是有明确说明这种行为的 `// By convention, to approximate the behavior of [Unmarshal] itself, // Unmarshalers implement UnmarshalJSON([]byte("null")) as a no-op.`

https://cs.opensource.google/go/go/+/master:src/encoding/json/decode.go;l=117-121;drc=dac9b9ddbd5160c5f4552410f5f8281bd5eed38c

对于 LZ 这种场景,比较好的方案是自己定义一个类型别名,然后给这个类型实现自己的 Unmarshal 接口,实现很简单。如果 LZ 的场景有很多的类型都要考虑 null 要报错,那我觉得可能是设计上有点问题了。

```
type Int int

func (i *Int) UnmarshalJSON(bs []byte) error {
if len(bs) == 0 || bytes.Equal(bs, []byte("null")) {
return fmt.Errorf("need a value")
}
val, err := strconv.ParseInt(string(bs), 10, 64)
if err != nil {
return err
}
*i = Int(val)
return nil
}

type Req struct {
ID Int `json:"id"`
}

var _ json.Unmarshaler = (*Int)(nil)

func TestNULLJSON(t *testing.T) {
var r Req
var args = []struct {
payload []byte
err bool
}{
{
[]byte(`{"id": null}`),
true,
},
{
[]byte(`{"id": 0}`),
false,
},
}
for idx, arg := range args {
if err := json.Unmarshal(arg.payload, &r); (err == nil) == arg.err {
t.Fatalf("%d: want err: %v, but got: %+v", idx, arg.err, err)
}
}
}

```
liuidetmks
2023-09-20 08:37:52 +08:00
你声明的是 Int 这种基本类型,只是一段 32 比特的数据,不能表示 Null
既然你这么声明了,那么你就应该处理好零值,或者你换一个可以表示 null 的类型

结构体的 size 是固定的,必须初始化对应的成员。
lmw2616
2023-09-20 08:42:59 +08:00
json 中的 null 对应 go 中的零值
crackidz
2023-09-20 08:46:55 +08:00
因为大家有看文档,不会遇到你说的“严重问题”...
aloxaf
2023-09-20 08:59:29 +08:00
@rekulas #50
C 语言的反序列化库不了解,而且有必要和上世纪八十年代的语言比烂么……
至于 Rust ,那不就是 OP 想要的效果吗
tsanie
2023-09-20 09:07:41 +08:00
我说个对比你们就明白 op 的疑问了,.net 中的 System.Text.Json ,碰到这种情况会在反序列化 json 的时候抛出异常 "Cannot get the value of a token type 'Null' as a number."

record JsonTest(int price);

JsonSerializer.Deserialize<JsonTest>("{\"price\":null}");

==============

System.Text.Json.JsonException: 'The JSON value could not be converted to Test.JsonTest. Path: $.price | LineNumber: 0 | BytePositionInLine: 13.'

InvalidOperationException: Cannot get the value of a token type 'Null' as a number.
tsanie
2023-09-20 09:09:32 +08:00
而如果允许这个 price 传入 null 的话可以定义为 record JsonTest(int? price);
tsanie
2023-09-20 09:13:23 +08:00
如果必须前端传回 price ,且值可以为 int 或 null 的话就定义成这样,未传入 price 会抛出异常 'JSON deserialization for type 'Test.JsonTest' was missing required properties, including the following: price'

record JsonTest
{
[JsonPropertyName("price")]
public required int? Price { get; set; }
}
tianxin8431
2023-09-20 09:14:41 +08:00
v2 上大伙都这么学院派吗,对于业务来说这确实是个很令人难受的问题啊。类比 Java 的话,会有人在定义 vo 的时候用 int 而不用 Integer ,然后去判断 int 的值 != 0 吗?正常情况下肯定是判断是否为 null 啊。当然用指针不是不可以,额外多出的工作量不还是得自己来吗?
skiy
2023-09-20 09:16:09 +08:00
OP 搞错了。并不是将 null 解释为空值,而是你类型不对,它就会解析成默认值。因为这个 Data 实例的每个值都肯定得有值。

你就算
{"a":123, "b": "abc", "obj":999}

它照样会解析成
{"a":"", "b": 0, "obj":...}
jonsmith
2023-09-20 09:28:59 +08:00
前端能直接传价格?不考虑安全性?
aloxaf
2023-09-20 09:29:17 +08:00
@skiy
没有吧,我试了下类型不匹配还是会报错的

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

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

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

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

© 2021 V2EX