Gin Custom Validator

Posted by Elizabeth Huang on Thu, May 1, 2025

發現在 gin 框架寫自訂驗證器比我想像中困難,看來我被 Laravel 養太好了🤯

這篇分享自訂驗證器以及自訂錯誤訊息的寫法,紀錄一下以免以後忘記~

先看官方文件的範例吧

Gin 官方文件的範例是,有一支 API 需要輸入 check in 和 check out 的日期,並自訂一個驗證器 bookabledate,如果輸入時間在今天之後,則驗證通過

 1package main
 2
 3import (
 4  "net/http"
 5  "time"
 6
 7  "github.com/gin-gonic/gin"
 8  "github.com/gin-gonic/gin/binding"
 9  "github.com/go-playground/validator/v10"
10)
11
12// Booking contains binded and validated data.
13type Booking struct {
14  CheckIn  time.Time `form:"check_in" binding:"required,bookabledate" time_format:"2006-01-02"`
15  CheckOut time.Time `form:"check_out" binding:"required,gtfield=CheckIn,bookabledate" time_format:"2006-01-02"`
16}
17
18var bookableDate validator.Func = func(fl validator.FieldLevel) bool {
19  date, ok := fl.Field().Interface().(time.Time)
20  if ok {
21    today := time.Now()
22    if today.After(date) {
23      return false
24    }
25  }
26  return true
27}
28
29func main() {
30  route := gin.Default()
31
32  if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
33    v.RegisterValidation("bookabledate", bookableDate)
34  }
35
36  route.GET("/bookable", getBookable)
37  route.Run(":8085")
38}
39
40func getBookable(c *gin.Context) {
41  var b Booking
42  if err := c.ShouldBindWith(&b, binding.Query); err == nil {
43    c.JSON(http.StatusOK, gin.H{"message": "Booking dates are valid!"})
44  } else {
45    c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
46  }
47}

測試

1$ curl "localhost:8085/bookable?check_in=2018-04-16&check_out=2018-04-15"
2{"error":"Key: 'Booking.CheckIn' Error:Field validation for 'CheckIn' failed on the 'bookabledate' tag\nKey: 'Booking.CheckOut' Error:Field validation for 'CheckOut' failed on the 'gtfield' tag"}

翻譯錯誤訊息

來修改一下,用 field 作為 key,錯誤訊息作為 value 回傳,並且錯誤訊息翻譯為中文

 1package main
 2
 3import (
 4	"net/http"
 5	"time"
 6
 7	"github.com/gin-gonic/gin"
 8	"github.com/gin-gonic/gin/binding"
 9	"github.com/go-playground/locales/en"
10	"github.com/go-playground/locales/zh_Hant_TW"
11	ut "github.com/go-playground/universal-translator"
12	"github.com/go-playground/validator/v10"
13	zh_tw_translations "github.com/go-playground/validator/v10/translations/zh_tw"
14)
15
16// Booking contains binded and validated data.
17type Booking struct {
18	CheckIn  time.Time `form:"check_in" binding:"required,bookabledate" time_format:"2006-01-02"`
19	CheckOut time.Time `form:"check_out" binding:"required,gtfield=CheckIn,bookabledate" time_format:"2006-01-02"`
20}
21
22var bookableDate validator.Func = func(fl validator.FieldLevel) bool {
23	date, ok := fl.Field().Interface().(time.Time)
24	if ok {
25		today := time.Now()
26		if today.After(date) {
27			return false
28		}
29	}
30	return true
31}
32
33var trans ut.Translator
34
35func main() {
36	route := gin.Default()
37
38	if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
39		v.RegisterValidation("bookabledate", bookableDate)
40
41		tw := zh_Hant_TW.New()
42		en := en.New()
43
44		uni := ut.New(en, tw, en)
45
46		trans, _ = uni.GetTranslator("zh_Hant_TW")
47		zh_tw_translations.RegisterDefaultTranslations(v, trans)
48	}
49
50	route.GET("/bookable", getBookable)
51	route.Run(":8085")
52}
53
54func getBookable(c *gin.Context) {
55	var b Booking
56	if err := c.ShouldBindWith(&b, binding.Query); err == nil {
57		c.JSON(http.StatusOK, gin.H{"message": "Booking dates are valid!"})
58	} else {
59		errs := err.(validator.ValidationErrors)
60		c.JSON(http.StatusBadRequest, gin.H{"error": errs.Translate(trans)})
61	}
62}

測試

1$ curl "localhost:8085/bookable?check_in=2018-04-16&check_out=2018-04-15"
2{
3    "error":{
4        "Booking.CheckIn":"Key: 'Booking.CheckIn' Error:Field validation for 'CheckIn' failed on the 'bookabledate' tag",
5        "Booking.CheckOut":"CheckOut必須大於CheckIn"
6    }
7}

validator 包的 errors.go 可以看到, errs.Translate(trans) 回傳的是 ValidationErrorsTranslations,它的型別是 map[string]string,key 為 ns,value 為翻譯後的錯誤訊息

 1// 節錄
 2type ValidationErrorsTranslations map[string]string
 3type ValidationErrors []FieldError
 4
 5func (ve ValidationErrors) Translate(ut ut.Translator) ValidationErrorsTranslations {
 6
 7	trans := make(ValidationErrorsTranslations)
 8
 9	var fe *fieldError
10
11	for i := 0; i < len(ve); i++ {
12		fe = ve[i].(*fieldError)
13
14		// // in case an Anonymous struct was used, ensure that the key
15		// // would be 'Username' instead of ".Username"
16		// if len(fe.ns) > 0 && fe.ns[:1] == "." {
17		// 	trans[fe.ns[1:]] = fe.Translate(ut)
18		// 	continue
19		// }
20
21		trans[fe.ns] = fe.Translate(ut)
22	}
23
24	return trans
25}

自訂錯誤訊息

現在來給驗證器 bookabledate 定義一個錯誤訊息

我們可以改寫上面的 Translate(),如果 struct tag 是我們自訂的驗證器,則回傳對應的錯誤訊息

複習一下 interface value,是由 value 和 concrete type 組成的 tuple,所以呼叫 interface 的 method,等於呼叫 concrete type 的同名 method

也就是說,我們可以呼叫 FieldErrorNamespace()StructField() 等 method 取得 fieldError 的 struct field

 1func getBookable(c *gin.Context) {
 2	var b Booking
 3	if err := c.ShouldBindWith(&b, binding.Query); err == nil {
 4		c.JSON(http.StatusOK, gin.H{"message": "Booking dates are valid!"})
 5	} else {
 6		errs := err.(validator.ValidationErrors)
 7		c.JSON(http.StatusBadRequest, gin.H{"error": getErrorsTrans(errs)})
 8	}
 9}
10
11func getErrorsTrans(ve validator.ValidationErrors) validator.ValidationErrorsTranslations {
12	trans := make(validator.ValidationErrorsTranslations)
13
14	for i := 0; i < len(ve); i++ {
15		trans[ve[i].Namespace()] = getErrorMessage(ve[i])
16	}
17
18	return trans
19}
20
21func getErrorMessage(fe validator.FieldError) string {
22	switch fe.Tag() {
23	case "bookabledate":
24		return fe.StructField() + "的時間必須大於今天"
25	}
26	return fe.Translate(trans)
27}

測試

1$ curl "localhost:8085/bookable?check_in=2018-04-16&check_out=2028-04-17"
2{"error":{"Booking.CheckIn":"CheckIn的時間必須大於今天"}}

參考資料