發現在 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
也就是說,我們可以呼叫 FieldError
的 Namespace()
和 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的時間必須大於今天"}}