这篇文章上次修改于 671 天前,可能其部分内容已经发生变化,如有疑问可询问作者。
正文
今天下午在测试一个接口的时候,发现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
没有评论