绑定与校验

Hertz 支持的参数绑定与校验相关功能及用法。

使用方法

func main() {
	r := server.New()

    r.GET("/hello", func(c context.Context, ctx *app.RequestContext) {
        // 参数绑定需要配合特定的 go tag 使用
		type Test struct {
            A string `query:"a" vd:"$!='Hertz'"`
        }

        // BindAndValidate
        var req Test
        err := ctx.BindAndValidate(&req)

        ...

	    // Bind 只做参数绑定
        req = Test{}
        err = ctx.Bind(&req)

        ...

        // Validate,需要使用 "vd" tag
        err = ctx.Validate(&req)

        ...
    })
...
}

全部 API

hertz version >= v0.7.0

API 说明
ctx.BindAndValidate 利用下述的 go-tag 进行参数绑定,并在绑定成功后做一次参数校验 (如果有校验 tag 的话)
ctx.Bind BindAndValidate 但是不做参数校验
ctx.BindQuery 绑定所有 Query 参数,相当于给每一个 field 声明一个 query tag,适用于没写 tag 的场景
ctx.BindHeader 绑定所有 Header 参数,相当于给每一个 field 声明一个 header tag,适用于没写 tag 的场景
ctx.BindPath 绑定所有 Path 参数,相当于给每一个 field 声明一个 path tag,适用于没写 tag 的场景
ctx.BindForm 绑定所有 Form 参数,相当于给每一个 field 声明一个 form tag,需要 Content-Type 为:application/x-www-form-urlencoded/multipart/form-data, 适用于没写 tag 的场景
ctx.BindJSON 绑定 JSON Body,调用 json.Unmarshal() 进行反序列化,需要 Body 为 application/json 格式
ctx.BindProtobuf 绑定 Protobuf Body,调用 proto.Unmarshal() 进行反序列化,需要 Body 为 application/x-protobuf 格式
ctx.BindByContentType 根据 Content-Type 来自动选择绑定的方法,其中 GET 请求会调用 BindQuery, 带有 Body 的请求会根据 Content-Type 自动选择
ctx.Validate 进行参数校验,需要校验 tag 配合使用 (默认使用 vd tag 校验)

支持的 tag 及参数绑定优先级

支持的 tag

不通过 IDL 生成代码时若字段不添加任何 tag 则会遍历所有 tag 并按照优先级绑定参数,添加 tag 则会根据对应的 tag 按照优先级去绑定参数。

通过 IDL 生成代码时若不添加 api 注解 则字段默认添加 formjsonquery tag,添加 api 注解 会为字段添加相应需求的 tag。

go tag 说明
path 绑定 url 上的路径参数,相当于 hertz 路由 :param*param 中拿到的参数。例如:如果定义的路由为:/v:version/example,可以把 path 的参数指定为路由参数:path:"version",此时,url: http://127.0.0.1:8888/v1/example,可以绑定path参数"1"
form 绑定请求的 body 内容。content-type -> multipart/form-dataapplication/x-www-form-urlencoded,绑定 form 的 key-value
query 绑定请求的 query 参数
cookie 绑定请求的 cookie 参数
header 绑定请求的 header 参数
json 绑定请求的 body 内容 content-type -> application/json,绑定 json 参数
raw_body 绑定请求的原始 body(bytes),绑定的字段名不指定,也能绑定参数。(注:raw_body 绑定优先级最低,当指定多个 tag 时,一旦其他 tag 成功绑定参数,则不会绑定 body 内容。)
vd 参数校验,校验语法
default 设置默认值

参数绑定优先级

path > form > query > cookie > header > json > raw_body

注:如果请求的 content-type 为 application/json,使用 BindAndValidate, Bind 方法会在参数绑定前做一次 json unmarshal 处理。

必传参数

通过在 tag 中添加 required,可以将参数标记为必传。当绑定失败时 BindBindAndValidate 将会返回错误。当多个 tag 包含 required 时,将会按照优先级绑定。如果所有 tag 都没有绑定上,则会返回错误。

type TagRequiredReq struct {
	// 当 JSON 中没有 hertz 字段时,会返回 required 错误
	Hertz string `json:"hertz,required"`
	// 当 query 和 JSON 中同时没有 kitex 字段时,会返回 required 错误
	Kitex string `query:"kitex,required" json:"kitex,required" `
}

常用配置

hertz 在 v0.7.0 版本对 参数绑定校验 进行了重构,重构后配置的行为发生变更,下面将分别介绍
如果还想使用之前的绑定器,目前已把其实现放到了 hertz-contrib/binding 下,可通过自定义 binder 引入

自定义 binder

hertz version >= v0.7.0 支持

需要实现 Binder 接口,并通过配置方式注入到 hertz engine

type Binder interface {
	Name() string // 绑定器的名字
	// 下面为各种绑定方法
	Bind(*protocol.Request, interface{}, param.Params) error
	BindAndValidate(*protocol.Request, interface{}, param.Params) error
	BindQuery(*protocol.Request, interface{}) error
	BindHeader(*protocol.Request, interface{}) error
	BindPath(*protocol.Request, interface{}, param.Params) error
	BindForm(*protocol.Request, interface{}) error
	BindJSON(*protocol.Request, interface{}) error
	BindProtobuf(*protocol.Request, interface{}) error
}

注入示例


func main() {
    // 通过配置的方式注入自定义 binder
    h := server.New(server.WithCustomBinder(&mockBinder{}))
    ...
    h.Spin()
}


type mockBinder struct{}

func (m *mockBinder) Name() string {
	return "test binder"
}

func (m *mockBinder) Bind(request *protocol.Request, i interface{}, params param.Params) error {
	return nil
}

func (m *mockBinder) BindAndValidate(request *protocol.Request, i interface{}, params param.Params) error {
	return fmt.Errorf("test binder")
}

func (m *mockBinder) BindQuery(request *protocol.Request, i interface{}) error {
	return nil
}

func (m *mockBinder) BindHeader(request *protocol.Request, i interface{}) error {
	return nil
}

func (m *mockBinder) BindPath(request *protocol.Request, i interface{}, params param.Params) error {
	return nil
}

func (m *mockBinder) BindForm(request *protocol.Request, i interface{}) error {
	return nil
}

func (m *mockBinder) BindJSON(request *protocol.Request, i interface{}) error {
	return nil
}

func (m *mockBinder) BindProtobuf(request *protocol.Request, i interface{}) error {
	return nil
}

目前已拓展的绑定器:

自定义 validator

hertz version >= v0.7.0 支持

需要实现 Validator 接口,并通过配置方式注入到 hertz engine

type StructValidator interface {
    ValidateStruct(interface{}) error // 校验函数
    Engine() interface{} // 返回底层的 Validator
    ValidateTag() string // 校验的 tag, 声明校验器使用的 tag
}

注入示例


func main() {
	// 通过配置的方式注入自定义 binder
    h := server.New(server.WithCustomValidator(&mockValidator{}))
    ...
    h.Spin()
}

type mockValidator struct{}

func (m *mockValidator) ValidateStruct(interface{}) error {
    return fmt.Errorf("test mock validator")
}

func (m *mockValidator) Engine() interface{} {
    return nil
}

func (m *mockValidator) ValidateTag() string {
    return "vt"
}

目前已拓展的校验器:

自定义 bind 和 validate 的 Error

在绑定参数发生错误和参数校验失败的时候,用户可以自定义 Error 的内容,使用方法如下:
hertz version >= v0.7.0

暂不支持自定义 bind error

自定义 validate error:

package main
import (
	"github.com/cloudwego/hertz/pkg/app/server/binding"
	"github.com/cloudwego/hertz/pkg/app/server"
)

type ValidateError struct {
   ErrType, FailField, Msg string
}

// Error implements error interface.
func (e *ValidateError) Error() string {
   if e.Msg != "" {
      return e.ErrType + ": expr_path=" + e.FailField + ", cause=" + e.Msg
   }
   return e.ErrType + ": expr_path=" + e.FailField + ", cause=invalid"
}

func main() {
    validateConfig := &binding.ValidateConfig{}
    validateConfig.SetValidatorErrorFactory(func(failField, msg string) error {
        err := ValidateError{
            ErrType:   "validateErr",
            FailField: "[validateFailField]: " + failField,
            Msg:       "[validateErrMsg]: " + msg,
        }
        
        return &err
        })
    h := server.New(server.WithValidateConfig(validateConfig))
    ...
    h.Spin()
}

hertz version < v0.7.0
demo

import "github.com/cloudwego/hertz/pkg/app/server/binding"

type BindError struct {
   ErrType, FailField, Msg string
}

// Error implements error interface.
func (e *BindError) Error() string {
   if e.Msg != "" {
      return e.ErrType + ": expr_path=" + e.FailField + ", cause=" + e.Msg
   }
   return e.ErrType + ": expr_path=" + e.FailField + ", cause=invalid"
}

type ValidateError struct {
   ErrType, FailField, Msg string
}

// Error implements error interface.
func (e *ValidateError) Error() string {
   if e.Msg != "" {
      return e.ErrType + ": expr_path=" + e.FailField + ", cause=" + e.Msg
   }
   return e.ErrType + ": expr_path=" + e.FailField + ", cause=invalid"
}

func init() {
    CustomBindErrFunc := func(failField, msg string) error {
       err := BindError{
          ErrType:   "bindErr",
          FailField: "[bindFailField]: " + failField,
          Msg:       "[bindErrMsg]: " + msg,
       }

       return &err
    }

    CustomValidateErrFunc := func(failField, msg string) error {
       err := ValidateError{
          ErrType:   "validateErr",
          FailField: "[validateFailField]: " + failField,
          Msg:       "[validateErrMsg]: " + msg,
       }

       return &err
    }

    binding.SetErrorFactory(CustomBindErrFunc, CustomValidateErrFunc)
}

自定义类型解析

在参数绑定的时候,针对某些特殊类型,当默认行为无法满足需求时,可使用自定义类型解析来解决,使用方法如下:
hertz version >= v0.7.0

package main

import (
    "github.com/cloudwego/hertz/pkg/app/server/binding"
    "github.com/cloudwego/hertz/pkg/app/server"
)

type Nested struct {
   B string
   C string
}

type TestBind struct {
   A Nested `query:"a,required"`
}

func main() {
    bindConfig := &binding.BindConfig{}
	// v0.7.0 重构后,在原基础上增加了请求 Request 内容以及路由参数,可方便用户更加灵活的自定义类型解析
	// 注意:只有 tag 成功匹配后,才会走到自定义的逻辑
    bindConfig.MustRegTypeUnmarshal(reflect.TypeOf(Nested{}), func(req *protocol.Request, params param.Params, text string) (reflect.Value, error) {
        if text == "" {
            return reflect.ValueOf(Nested{}), nil
        }
        val := Nested{
            B: text[:5],
            C: text[5:],
        }
        // 此外,也可以利用 req, params 来获取其他参数进行参数绑定
        return reflect.ValueOf(val), nil
    })
    h := server.New(server.WithBindConfig(bindConfig))
    
    ...
    h.Spin()
}

hertz version < v0.7.0

demo

import "github.com/cloudwego/hertz/pkg/app/server/binding"

type Nested struct {
   B string
   C string
}

type TestBind struct {
   A Nested `query:"a,required"`
}

func init() {
   binding.MustRegTypeUnmarshal(reflect.TypeOf(Nested{}), func(v string, emptyAsZero bool) (reflect.Value, error) {
      if v == "" && emptyAsZero {
         return reflect.ValueOf(Nested{}), nil
      }
      val := Nested{
         B: v[:5],
         C: v[5:],
      }
      return reflect.ValueOf(val), nil
   })
}

自定义验证函数

可以通过注册自定义验证函数,在 vd 注解中实现复杂的验证逻辑:
hertz version >= v0.7.0

package main

import (
    "github.com/cloudwego/hertz/pkg/app/server/binding"
    "github.com/cloudwego/hertz/pkg/app/server"
)

func main() {
    type Req struct {
        A int `query:"a" vd:"test($)"`
    }
    validateConfig := &binding.ValidateConfig{}
    validateConfig.MustRegValidateFunc("test", func(args ...interface{}) error {
        if len(args) != 1 {
            return fmt.Errorf("the args must be one")
        }
        s, _ := args[0].(string)
        if s == "123" {
            return fmt.Errorf("the args can not be 123")
        }
    return nil
    })
    h := server.New(server.WithValidateConfig(validateConfig))
    ...
    h.Spin()
}

hertz version < v0.7.0
demo

import "github.com/cloudwego/hertz/pkg/app/server/binding"

func init() {
    binding.MustRegValidateFunc("test", func(args ...interface{}) error {
       if len(args) != 1 {
          return fmt.Errorf("the args must be one")
       }
       s, _ := args[0].(string)
       if s == "123" {
          return fmt.Errorf("the args can not be 123")
       }
       return nil
    })
}

配置 looseZero

在一些场景下,前端有时候传来的信息只有 key 没有 value,这会导致绑定数值类型的时候报错;这时需要配置 looseZero 模式,使用方法如下: hertz version >= v0.7.0

package main

import (
    "github.com/cloudwego/hertz/pkg/app/server/binding"
    "github.com/cloudwego/hertz/pkg/app/server"
)

func main() {
    bindConfig := binding.NewBindConfig()
    // 默认 false,当前 Hertz Engine 下生效,多份 engine 实例之间不会冲突
    bindConfig.LooseZeroMode = true
    h := server.New(server.WithBindConfig(bindConfig))
    ...
    h.Spin()
}

hertz version < v0.7.0

import "github.com/cloudwego/hertz/pkg/app/server/binding"

func init() {
    // 默认 false,全局生效,如果其他组件也使用相关配置,可能会发生配置冲突
    binding.SetLooseZeroMode(true)
}

配置其他 json unmarshal 库

在绑定参数的时候,如果请求体为 json,会进行一次 json 的 unmarshal,如果用户需要使用特定的 json 库可以自行配置(hertz 默认使用开源 json 库 sonic )。使用方法如下:
hertz version >= v0.7.0

import (
    "github.com/cloudwego/hertz/pkg/app/server/binding"
    "github.com/cloudwego/hertz/pkg/app/server"
)

func main() {
    bindConfig := binding.NewBindConfig()
    bindConfig.UseStdJSONUnmarshaler() // 使用标准库作为 JSON 反序列化工具,hertz 默认使用 sonic 作为 JSON 反序列化器
    //bindConfig.UseThirdPartyJSONUnmarshaler(sonic.Unmarshal) // 使用 sonic 作为 JSON 反序列化器
    h := server.New(server.WithBindConfig(bindConfig))
    ...
    h.Spin()
}

hertz version < v0.7.0

import "github.com/cloudwego/hertz/pkg/app/server/binding"

func init() {
    // 使用标准库作为 JSON 反序列化工具
    binding.UseStdJSONUnmarshaler()

    // 使用 GJSON 作为 JSON 反序列化工具
    binding.UseGJSONUnmarshaler()

    // 使用第三方 JSON 库作为 JSON 反序列化工具
    binding.UseThirdPartyJSONUnmarshaler()
}

设置默认值

重构前后使用方式都一样

参数支持 default tag 进行默认值的配置,使用方法如下:

// 生成的代码
type UserInfoResponse struct {
   NickName string `default:"Hertz" json:"NickName" query:"nickname"`
}

绑定文件

重构前后使用方式一样,IDL 场景不支持文件绑定 文件类型需为:multipart.FileHeader

参数绑定支持绑定文件,使用方法如下:

// 需要请求的 content-type 为:multipart/form-data
type FileParas struct {
   F   *multipart.FileHeader `form:"F1"`
}

h.POST("/upload", func(ctx context.Context, c *app.RequestContext) {
   var req FileParas
   err := binding.BindAndValidate(c, &req)
})

常见问题分析

1. string 转 int 报错:json: cannot unmarshal string into Go struct field xxx of type intxx

原因:默认不支持 stringint 互转

解决方法:

  • 建议使用标准包 json 的 string tag, 例如:

    A int `json:"A, string"`
    
  • 配置其他支持这种行为的 json 库


最后修改 January 18, 2024 : Upload volo blog (#936) (1fc8abb)