这篇文章上次修改于 504 天前,可能其部分内容已经发生变化,如有疑问可询问作者。

正文

今天下午在测试一个接口的时候,发现gin的ShouldBindBodyWith函数一直无法正常绑定,报错如下:

{"level":"warning","msg":"Key: 'EditUserRequest.Body.Source' Error:Field validation for 'Source' failed on the 'required' tag","time":"2022-09-22T14:15:57+08:00"}

一般来说这个问题是由于Source的值没有被写入JSON报文造成的,但是再三确认拼写和结构后,我仍然没有发现任何问题。

源码和报文溯源

结构体:

type DATA struct {
    Username   string `json:"Username" binding:"required"`
    Permission int    `json:"Permission" binding:"required"`
    Source     int    `json:"Source" binding:"required"`
}

源码:

    var Request EditUserRequest
    err := Params.RequestContext.ShouldBindBodyWith(&Request, binding.JSON)
    if err != nil {
        Logger.Warn(err)
        return "Failed", common.ParamsError, errors.New(common.ErrInvaildMessage)
    }

报文:

{
    "Token": "Token",
    "Body": {
        "Username": "C44",
        "Permission": 4,
        "Source": 0
    }
}

源码分析

首先我们得弄清楚一件事,我在源码中调用的ShouldBindBodyWith函数,其本质上是Gin框架对encoding/json的一次再封装,证据如下:
package gin中:

// ShouldBindBodyWith函数中,实际上明确了对binding中定义的函数的调用
func (c *Context) ShouldBindBodyWith(obj any, bb binding.BindingBody) (err error) {
        ...以上略
    return bb.BindBody(body, obj)
}

package binding中:

// 此处定义了binding.JSON对象
var (
    JSON          = jsonBinding{}
        ...以下略...
)

// 此处是binding.JSON的实质
type jsonBinding struct{}

func (jsonBinding) Name() string {
    return "json"
}

// 会被gin.Context.ShouldBindBodyWith函数调用的Bind函数
func (jsonBinding) Bind(req *http.Request, obj any) error {
    if req == nil || req.Body == nil {
        return errors.New("invalid request")
    }
    return decodeJSON(req.Body, obj)
}

// 这个文件import的包,注意"github.com/gin-gonic/gin/internal/json"
import (
    "bytes"
    "errors"
    "io"
    "net/http"

    "github.com/gin-gonic/gin/internal/json"
)

// JSON解码的底层函数
func decodeJSON(r io.Reader, obj any) error {
    decoder := json.NewDecoder(r)
    if EnableDecoderUseNumber {
        decoder.UseNumber()
    }
    if EnableDecoderDisallowUnknownFields {
        decoder.DisallowUnknownFields()
    }
    if err := decoder.Decode(obj); err != nil {
        return err
    }
    return validate(obj)
}

根据上述源码,我们能够知道一件事: 对于JSON格式的报文,gin最终调用的核心库是 github.com/gin-gonic/gin/internal/json 。而这个库又是什么呢?请看源码:

// "github.com/gin-gonic/gin/internal/json"
package json

import "encoding/json"

var (
    // Marshal is exported by gin/json package.
    Marshal = json.Marshal
    // Unmarshal is exported by gin/json package.
    Unmarshal = json.Unmarshal
    // MarshalIndent is exported by gin/json package.
    MarshalIndent = json.MarshalIndent
    // NewDecoder is exported by gin/json package.
    NewDecoder = json.NewDecoder
    // NewEncoder is exported by gin/json package.
    NewEncoder = json.NewEncoder
)

综上所述,这个ShouldBindBodyWith(JSON)实质上就是encoding/json的一个再封装而已。
我们基于此就可以进一步分析开篇提到的问题了。

问题分析

造成此问题的原因是,在大部分go反序列化库(包括encoding/json)中,对于所有Golang变量,他们都有一个默认的值,对于slice类是nil,对于string是"",对于int及其衍生类型则为0。
这意味着一件事,那就是当你没有传入这个字段的时候,encoding/json库会采用这个默认值,进而反馈到gin框架内。
我举个例子,假设你压根没有在报文里写入Source字段,那么在反序列化的时候,Source字段就会被encoding/json取值为0。

但是ShouldBindBodyWith甚至gin框架中所有关于BindBody功能的函数,都是基于我上面提到的默认值特性进行判断的。

再回到我发送的报文这边:

        "Source": 0

我规定了Source为0对吧,但是假设我不传这个值,在encoding/json反序列化的结果里,这个值也会是0。
我上面说了,“但是ShouldBindBodyWith甚至gin框架中所有关于BindBody功能的函数,都是基于我上面提到的默认值特性进行判断的。”,那么这边,我传入了0和我压根不传入,在encoding/json反序列化的结果里体现都是0,那么ShouldBindBodyWith怎么判断我是否传入了呢?答案自然是判断为我没有传入。
而ShouldBindBodyWith函数在没有加required tag,也就是:

    Source     int    `json:"Source" binding:"required"`

binding:"required"这个tag的时候,是不会报错的,但一旦加了,它就会根据判断结果进行报错,从而出现了我们开篇提到的报错。

解决方案

想必如果您能理解前文的话,应该也知道怎么解决了,我这是直接把binding:"required"这个tag去掉了,您也可以根据自己的见解讨论这个问题并予以解决,我的方案仅供参考。

Q.E.D
C4a15Wh
2022-09-22