Loading... # 正文 今天下午在测试一个接口的时候,发现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 最后修改:2025 年 01 月 15 日 © 允许规范转载 赞 如果这对你有用,我乐意之至。