golang validator 自定义验证器 定制错误消息

4,064 阅读2分钟

问题

在前后端分离的web服务中,通常需要对前端的请求参数做一些校验,比如年龄、身高范围,密码长度,邮件格式等等,以确保请求参数的合法性。

当前github中go语言相关,stars数最多的第三方库是:github.com/go-playgrou…

go get:

go get github.com/go-playground/validator/v10

import:

import "github.com/go-playground/validator/v10"

这个库支持很多验证规则,详情见:documentation

本文主要是介绍如何给指定tag自定义验证器,以及如何定制错误消息返回给调用方

相关实现

下面先进行简要说明,然后给出代码实现。

  1. 将验证规则写到结构体的tag中,用validate字段标识;
  2. 调用validator.New().RegisterValidation(tag string, fn Func, ...)函数进行tag和自定义验证器的注册;
// RegisterValidation adds a validation with the given tag
//
// NOTES:
// - if the key already exists, the previous validation function will be replaced.
// - this method is not thread-safe it is intended that these all be registered prior to any validation
func (v *Validate) RegisterValidation(tag string, fn Func, callValidationEvenIfNull ...bool) error {
   return v.RegisterValidationCtx(tag, wrapFunc(fn), callValidationEvenIfNull...)
}
  1. 对验证器验证后得到的err进行断言err.(validator.ValidationErrors)
  2. 对断言后的ValidationError进行规则匹配,然后自定义返回给调用方的错误消息;

参考自:validator/examples/simple/main.go

package main

import (
   "errors"
   "strings"
   "testing"

   "github.com/go-playground/validator/v10"
   "github.com/sirupsen/logrus"
   "github.com/stretchr/testify/assert"
)

type People struct {
   Name string `json:"name" validate:"required"`
   // 如果某项为空,不需要验证,需要把omitempty这个tag放在最前面,如下所示。
   // 如果写成"isValidAge,omitempty"则不能到达上面的效果
   Age    int    `json:"age" validate:"omitempty,isValidAge"`
   Hobby  string `json:"hobby" validate:"isValidHobby,oneof=ball swim"`
   Height int    `json:"height" validate:"max=100,min=10"`
}

const (
   isValidAgeTag   = "isValidAge"
   isValidHobbyTag = "isValidHobby"
)

var (
   InvalidAgeErr   = errors.New("age should be between 0 and 200")
   InvalidHobbyErr = errors.New("hobby should be 'ball' or 'swim'")
)

func validAge(fl validator.FieldLevel) bool {
   age := fl.Field().Int()
   if age > 0 && age < 200 {
      return true
   }
   return false
}

func validHobby(fl validator.FieldLevel) bool {
   hobby := fl.Field().String()
   if hobby == "ball" || hobby == "swim" {
      return true
   }
   return false
}

var validateObj *validator.Validate

func registerValidation(tag string, fn validator.Func) {
   if err := validateObj.RegisterValidation(tag, fn); err != nil {
      logrus.Fatalf("register validator for '%s' error: %v", tag, err)
   }
}

func init() {
   validateObj = validator.New()

   registerValidation(isValidAgeTag, validAge)
   registerValidation(isValidHobbyTag, validHobby)
}

func validateStruct(s interface{}) error {
   err := validateObj.Struct(s)
   if err == nil {
      return nil
   }

   // refer to: https://github.com/go-playground/validator/issues/559
   for _, validateErr := range err.(validator.ValidationErrors) {
      // only check first error
      switch validateErr.Tag() {
      case isValidAgeTag:
         // return custom error message
         return InvalidAgeErr
      case isValidHobbyTag:
         return InvalidHobbyErr
      default:
         return err
      }
   }

   return err
}

func TestValidateStruct(t *testing.T) {
   cases := []struct {
      people     People
      isValid    bool
      errContain string
   }{
      {People{}, false, "Name"},
      {People{Name: "tom"}, false, InvalidHobbyErr.Error()}, // 验证 Age => omitempty tag
      {People{Name: "tom", Age: -10}, false, InvalidAgeErr.Error()},
      {People{Name: "tom", Age: 10}, false, InvalidHobbyErr.Error()},
      {People{Name: "tom", Age: 10, Hobby: "ball"}, false, "Height"},
      {People{Name: "tom", Age: 10, Hobby: "ball", Height: 10}, true, ""},
   }

   for _, item := range cases {
      err := validateStruct(item.people)
      if item.isValid {
         assert.NoError(t, err)
      } else {
         assert.True(t, strings.Contains(err.Error(), item.errContain))
      }
   }
}

参考

最后

大家好,如果觉得有用,可以悄悄给我点个赞[😄],也可以关注微信公众号「那只猴子」。