bigywater.tech
Gin · 35

JSON 绑定:DisallowUnknownFields 与 UseNumber

前端多传了字段、写错了字段名,Gin 默认静悄悄地忽略;想要严格怎么办?大整数放在 map 里为什么变了?这篇用同一批请求体测 Gin 的 JSON 绑定默认有多宽松,打开 DisallowUnknownFields 与 UseNumber 后行为怎么变,以及这两个全局开关的影响面。

不报错
JSON 末尾带 garbage 时绑定的结果:只解第一个值,后面的内容被忽略
…992 vs …993
any 字段里 9007199254740993 默认被解成的 float64 与 UseNumber 保持的值
3 条路由
全局开关同时影响 ShouldBindJSON、ShouldBindBodyWith、ShouldBind

默认很宽松

实测 · 本机执行输出
  binding/json.go 的 decodeJSON(Gin v1.12.0):
      44 | func decodeJSON(r io.Reader, obj any) error {
      45 |     decoder := json.API.NewDecoder(r)
      46 |     if EnableDecoderUseNumber {
      47 |         decoder.UseNumber()
      48 |     }
      49 |     if EnableDecoderDisallowUnknownFields {
      50 |         decoder.DisallowUnknownFields()
      51 |     }
      52 |     if err := decoder.Decode(obj); err != nil {
      53 |         return err
      54 |     }
      55 |     return validate(obj)
      56 | }
    {"name":"ann","age":30}                        → {Name:ann Age:30} err=<nil>
    {"name":"ann","age":30,"extra":1,"nested":{"a":[1,2]}} → {Name:ann Age:30} err=<nil>
    {"NAME":"ann","Age":30}                        → {Name:ann Age:30} err=<nil>
    {"name":"a","name":"b"}                        → {Name:b Age:0} err=<nil>
    {"name":"ann","age":30} {"name":"second"}      → {Name:ann Age:30} err=<nil>
    {"name":"ann","age":30} garbage                → {Name:ann Age:30} err=<nil>
    {"name":"ann","age":30}}}}                     → {Name:ann Age:30} err=<nil>
    {"name":"ann","age":30.0}                      → {Name:ann Age:0} err=json: cannot unmarshal number 30.0 into Go struct field P.age of type int
    {"name":"ann","age":30.5}                      → {Name:ann Age:0} err=json: cannot unmarshal number 30.5 into Go struct field P.age of type int
    {"name":"ann","age":"30"}                      → {Name:ann Age:0} err=json: cannot unmarshal string into Go struct field P.age of type int
    {"name":null,"age":null}                       → {Name: Age:0} err=<nil>
    对照 json.Unmarshal 同一个“带垃圾”的输入 → {Name: Age:0} err=invalid character 'g' after top-level value

Gin 的 decodeJSON 用 Decode 解码第一个 JSON 值,不检查后面还有没有内容,所以拼接了垃圾的请求体照样通过(与 json.Unmarshal 不同)。多余字段、大小写不一致、重复键都不报错;类型对不上(30.0 给 int、字符串给 int)则会报错。

严格模式

实测 · 本机执行输出
    {"name":"ann"}                               → {Name:ann In:{Z:0} Items:[]} err=<nil>
    {"name":"ann","extra":1}                     → {Name:ann In:{Z:0} Items:[]} err=json: unknown field "extra"
    {"name":"ann","in":{"z":1,"y":2}}            → {Name:ann In:{Z:1} Items:[]} err=json: unknown field "y"
    {"items":[{"z":1},{"q":2}]}                  → {Name: In:{Z:0} Items:[{Z:1} {Z:0}]} err=json: unknown field "q"
    {"NAME":"ann"}                               → {Name:ann In:{Z:0} Items:[]} err=<nil>
    {"name":"ann"} garbage                       → {Name:ann In:{Z:0} Items:[]} err=<nil>

把 binding.EnableDecoderDisallowUnknownFields 设为 true,未知字段(包括嵌套对象和数组元素里的)会报 json: unknown field。但错误之前的字段已经赋值,并且键的大小写、末尾垃圾仍然宽松,所以它只是“防止拼错字段名”,不是完整的严格校验。

大整数

实测 · 本机执行输出
    UseNumber=false → ID=9.007199254740992e+15(float64) m.big=1.2345678901234567e+19(float64) m.f=1.1 m.small=7(float64) I64=9007199254740993 err=<nil>
    UseNumber=true  → ID=9007199254740993(json.Number) m.big=12345678901234567890(json.Number) m.f=1.10 m.small=7(json.Number) I64=9007199254740993 err=<nil>
    对照:9007199254740993 的 float64 值 = 9007199254740992

interface{} 里的 JSON 数字默认是 float64,超过 253 的整数会静默丢精度。EnableDecoderUseNumber 让它们变成 json.Number 保持原文。声明成 int64 的字段本来就精确,不需要这个开关;只在你确实要用 map[string]any 接收 ID 之类大整数时才有用。

全局开关的代价

实测 · 本机执行输出
    DisallowUnknownFields=false:
      /a ShouldBindJSON err=<nil>
      /b ShouldBindBodyWith err=<nil>
      /c ShouldBind err=<nil>
    DisallowUnknownFields=true :
      /a ShouldBindJSON err=json: unknown field "extra"
      /b ShouldBindBodyWith err=json: unknown field "extra"
      /c ShouldBind err=json: unknown field "extra"
    DisallowUnknownFields=false:
      /a ShouldBindJSON err=<nil>
      /b ShouldBindBodyWith err=<nil>
      /c ShouldBind err=<nil>
    只想让某一个接口严格:自己用 json.NewDecoder + DisallowUnknownFields,不用全局开关:
      400 json: unknown field "extra"

这两个变量是包级别的,一旦设置,整个进程的所有 JSON 绑定都按新规则走,包括第三方库里调用的 ShouldBindJSON;在运行中切换更会让同一个接口前后行为不一致。只想让个别接口严格时,在该接口里自己创建 json.Decoder 最安全。

分步演示

JSON 严格模式
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Go 1.27.1(brew)/ Gin v1.12.0(go.mod 里 require,源码在模块缓存里);用 go run / go test 直接编译运行,需要库时经代理从 proxy.golang.org 下载;HTTP 实验用 httptest 或本机回环端口(9700 以上);计时用 time.Now / testing.B,数字每次运行会有小幅抖动。请求用 httptest 直接调用 ServeHTTP;binding 包的两个开关用代码赋值;JSON 编解码用 Gin 默认的 encoding/json(没有开 jsoniter / sonic 等构建标签);每种情形各一次
    • 没有验证:用 sonic、go-json 等替代 JSON 库(构建标签)时这些行为的差异——Gin 源码注释说它们对某些错误的处理不同;超大请求体、深层嵌套对象的限制;XML、YAML 绑定的严格程度;自定义 UnmarshalJSON 与这两个开关的交互
    京ICP备2026067502号-1京公网安备11010502063487号