banner
biuaxia

biuaxia

"万物皆有裂痕,那是光进来的地方。"
github
bilibili
tg_channel

Customizing Gin's Validator Error Messages and Response Structure

title: Customizing Gin's Validator Error Messages and Response Structure
date: 2021-11-14 19:02:00
toc: true
category:

  • Golang
  • Gin
    tags:
  • Golang
  • golang
  • Go
  • go
  • Gin
  • gin
  • Validator
  • validator
  • functionality
  • validation
  • customization
  • implementation
  • response
  • structure
  • error
  • message
  • localization
  • translation

Introduction#

When using the Validator functionality in Golang, you will find that many error messages are in English. For example

{  
    "code": -1,  
    "msg": {  
        "SingUpForm.Age": "Key: 'SingUpForm.Age' Error:Field validation for 'Age' failed on the 'gte' tag",  
        "SingUpForm.Email": "Key: 'SingUpForm.Email' Error:Field validation for 'Email' failed on the 'required' tag",  
        "SingUpForm.Name": "Key: 'SingUpForm.Name' Error:Field validation for 'Name' failed on the 'required' tag",  
        "SingUpForm.Password": "Key: 'SingUpForm.Password' Error:Field validation for 'Password' failed on the 'required' tag",  
        "SingUpForm.RePassword": "Key: 'SingUpForm.RePassword' Error:Field validation for 'RePassword' failed on the 'required' tag"  
    }  
}  

This can cause reading difficulties for API callers. After reviewing the official documentation, it was found that translation work has already been done, and error messages can be directly modified to the specified language.

However, it should be noted that currently go-playground/validator only supports 13 languages, refer to the official directory for details: validator/translations at master · go-playground/validator

Snipaste20211114190832.png

Localizing Error Messages#

Let's get straight to the code; the logic is clear. Each time an error message appears, convert it through err.(validator.ValidationErrors) to get the exception object validator.ValidationErrors, and call its func (ve ValidationErrors) Translate(ut ut.Translator) ValidationErrorsTranslations {} method.

package main  

import (  
    "fmt"  
    "net/http"  

    "github.com/gin-gonic/gin"  
    "github.com/gin-gonic/gin/binding"  
    "github.com/go-playground/locales/en"  
    "github.com/go-playground/locales/zh"  
    ut "github.com/go-playground/universal-translator"  
    "github.com/go-playground/validator/v10"  

    en_translations "github.com/go-playground/validator/v10/translations/en"  
    zh_translations "github.com/go-playground/validator/v10/translations/zh"  
)  

// Translate validation messages  
func main() {  
    if err := InitTranslate("zh"); err != nil {  
        fmt.Println("Error initializing translator")  
        return  
    }  

    r := gin.Default()  

    r.POST("/", func(c *gin.Context) {  
        var singUpForm SingUpForm  
        if err := c.ShouldBind(&singUpForm); nil != err {  
            // An error occurred  
            errors, ok := err.(validator.ValidationErrors)  
            if !ok {  
                c.JSON(http.StatusOK, gin.H{  
                    "code": -1,  
                    "msg":  fmt.Errorf("error getting translation of error message, %s", err.Error()),  
                })  
                return  
            }  

            c.JSON(http.StatusBadRequest, gin.H{  
                "code": -1,  
                "msg":  errors.Translate(translator),  
            })  

            return  
        }  

        c.JSON(http.StatusOK, gin.H{  
            "code": 0,  
            "msg":  "Login successful",  
        })  
    })  

    // Listen and serve on 0.0.0.0:8080  
    r.Run(":8080")  
}  

type SingUpForm struct {  
    Age        uint8  `json:"age" binding:"gte=1,lte=130"`  
    Name       string `json:"name" binding:"required,min=3"`  
    Email      string `json:"email" binding:"required,email"`  
    Password   string `json:"password" binding:"required"`  
    RePassword string `json:"rePassword" binding:"required,eqfield=Password"`  
}  

// use a single instance , it caches struct info  
var (  
    uni        *ut.UniversalTranslator  
    validate   *validator.Validate  
    translator ut.Translator  
)  

func InitTranslate(locale string) (err error) {  
    // Modify the validator engine properties in the gin framework to achieve translation effects  
    engine, ok := binding.Validator.Engine().(*validator.Validate)  
    if !ok {  
        fmt.Println("Error getting gin framework's validator")  
    }  

    zhT := zh.New()  
    enT := en.New()  

    uni := ut.New(enT, zhT, enT)  

    translator, ok = uni.GetTranslator(locale)  
    if !ok {  
        return fmt.Errorf("uni.GetTranslator(%s)", locale)  
    }  

    switch locale {  
    case "en":  
        en_translations.RegisterDefaultTranslations(engine, translator)  
    case "zh":  
        zh_translations.RegisterDefaultTranslations(engine, translator)  
    }  

    return  
}  

The modified effect is:

{  
    "code": -1,  
    "msg": {  
        "SingUpForm.Age": "Age must be greater than or equal to 1",  
        "SingUpForm.Email": "Email is a required field",  
        "SingUpForm.Name": "Name is a required field",  
        "SingUpForm.Password": "Password is a required field",  
        "SingUpForm.RePassword": "RePassword is a required field"  
    }  
}  

Response Content Structure Property Names Start with Lowercase#

The original response content:

{  
    "code": -1,  
    "msg": {  
        "SingUpForm.Age": "Age must be greater than or equal to 1",  
        "SingUpForm.Email": "Email is a required field",  
        "SingUpForm.Name": "Name is a required field",  
        "SingUpForm.Password": "Password is a required field",  
        "SingUpForm.RePassword": "RePassword is a required field"  
    }  
}  

The modified content is:

{  
    "code": -1,  
    "msg": {  
        "SingUpForm.age": "age must be greater than or equal to 1",  
        "SingUpForm.email": "email is a required field",  
        "SingUpForm.name": "name is a required field",  
        "SingUpForm.password": "password is a required field",  
        "SingUpForm.rePassword": "rePassword is a required field"  
    }  
}  

The complete code is:

package main  

import (  
    "fmt"  
    "net/http"  
    "reflect"  
    "strings"  

    "github.com/gin-gonic/gin"  
    "github.com/gin-gonic/gin/binding"  
    "github.com/go-playground/locales/en"  
    "github.com/go-playground/locales/zh"  
    ut "github.com/go-playground/universal-translator"  
    "github.com/go-playground/validator/v10"  

    en_translations "github.com/go-playground/validator/v10/translations/en"  
    zh_translations "github.com/go-playground/validator/v10/translations/zh"  
)  

// Translate validation messages  
func main() {  
    if err := InitTranslate("zh"); err != nil {  
        fmt.Println("Error initializing translator")  
        return  
    }  

    r := gin.Default()  

    r.POST("/", func(c *gin.Context) {  
        var singUpForm SingUpForm  
        if err := c.ShouldBind(&singUpForm); nil != err {  
            // An error occurred  
            errors, ok := err.(validator.ValidationErrors)  
            if !ok {  
                c.JSON(http.StatusOK, gin.H{  
                    "code": -1,  
                    "msg":  fmt.Errorf("error getting translation of error message, %s", err.Error()),  
                })  
                return  
            }  

            c.JSON(http.StatusBadRequest, gin.H{  
                "code": -1,  
                "msg":  errors.Translate(translator),  
            })  

            return  
        }  

        c.JSON(http.StatusOK, gin.H{  
            "code": 0,  
            "msg":  "Login successful",  
        })  
    })  

    // Listen and serve on 0.0.0.0:8080  
    r.Run(":8080")  
}  

type SingUpForm struct {  
    Age        uint8  `json:"age" binding:"gte=1,lte=130"`  
    Name       string `json:"name" binding:"required,min=3"`  
    Email      string `json:"email" binding:"required,email"`  
    Password   string `json:"password" binding:"required"`  
    RePassword string `json:"rePassword" binding:"required,eqfield=Password"`  
}  

// use a single instance , it caches struct info  
var (  
    uni        *ut.UniversalTranslator  
    validate   *validator.Validate  
    translator ut.Translator  
)  

func InitTranslate(locale string) (err error) {  
    // Modify the validator engine properties in the gin framework to achieve translation effects  
    engine, ok := binding.Validator.Engine().(*validator.Validate)  
    if !ok {  
        fmt.Println("Error getting gin framework's validator")  
    }  

    // Implement response content tag starting with lowercase  
    engine.RegisterTagNameFunc(func(field reflect.StructField) string {  
        // For example, RePassword string `json:"rePassword" binding:"required,eqfield=Password"`  
        // Then jsonTag is rePassword  
        jsonTag := field.Tag.Get("json")  
        // Split, for example strings.SplitN("a,b,c", ",", 2) gives [a b,c], strings.SplitN("a,b,c,d", ",", 2) gives [a b c,d]  
        splitN := strings.SplitN(jsonTag, ",", 2)  
        name := splitN[0]  
        if name == "-" {  
            return ""  
        }  
        return name  
    })  

    zhT := zh.New()  
    enT := en.New()  

    uni := ut.New(enT, zhT, enT)  

    translator, ok = uni.GetTranslator(locale)  
    if !ok {  
        return fmt.Errorf("uni.GetTranslator(%s)", locale)  
    }  

    switch locale {  
    case "en":  
        en_translations.RegisterDefaultTranslations(engine, translator)  
    case "zh":  
        zh_translations.RegisterDefaultTranslations(engine, translator)  
    }  

    return  
}  

// Remove the struct name from the response, e.g., `SingUpForm.age` becomes `age`  
func removeTopStruct(fields map[string]string) map[string]string {  
    resp := make(map[string]string)  
    for key, value := range fields {  
        resp[key[strings.Index(key, ".")+1:]] = value  
    }  
    return resp  
}  

Removing Struct from Response Content#

The original response content:

{  
    "code": -1,  
    "msg": {  
        "SingUpForm.Age": "Age must be greater than or equal to 1",  
        "SingUpForm.Email": "Email is a required field",  
        "SingUpForm.Name": "Name is a required field",  
        "SingUpForm.Password": "Password is a required field",  
        "SingUpForm.RePassword": "RePassword is a required field"  
    }  
}  

The modified content is:

{  
    "code": -1,  
    "msg": {  
        "age": "age must be greater than or equal to 1",  
        "email": "email is a required field",  
        "name": "name is a required field",  
        "password": "password is a required field",  
        "rePassword": "rePassword is a required field"  
    }  
}  

The complete code is:

package main  

import (  
    "fmt"  
    "net/http"  
    "reflect"  
    "strings"  

    "github.com/gin-gonic/gin"  
    "github.com/gin-gonic/gin/binding"  
    "github.com/go-playground/locales/en"  
    "github.com/go-playground/locales/zh"  
    ut "github.com/go-playground/universal-translator"  
    "github.com/go-playground/validator/v10"  

    en_translations "github.com/go-playground/validator/v10/translations/en"  
    zh_translations "github.com/go-playground/validator/v10/translations/zh"  
)  

// Translate validation messages  
func main() {  
    if err := InitTranslate("zh"); err != nil {  
        fmt.Println("Error initializing translator")  
        return  
    }  

    r := gin.Default()  

    r.POST("/", func(c *gin.Context) {  
        var singUpForm SingUpForm  
        if err := c.ShouldBind(&singUpForm); nil != err {  
            // An error occurred  
            errors, ok := err.(validator.ValidationErrors)  
            if !ok {  
                c.JSON(http.StatusOK, gin.H{  
                    "code": -1,  
                    "msg":  fmt.Errorf("error getting translation of error message, %s", err.Error()),  
                })  
                return  
            }  

            c.JSON(http.StatusBadRequest, gin.H{  
                "code": -1,  
                "msg":  removeTopStruct(errors.Translate(translator)),  
            })  

            return  
        }  

        c.JSON(http.StatusOK, gin.H{  
            "code": 0,  
            "msg":  "Login successful",  
        })  
    })  

    // Listen and serve on 0.0.0.0:8080  
    r.Run(":8080")  
}  

type SingUpForm struct {  
    Age        uint8  `json:"age" binding:"gte=1,lte=130"`  
    Name       string `json:"name" binding:"required,min=3"`  
    Email      string `json:"email" binding:"required,email"`  
    Password   string `json:"password" binding:"required"`  
    RePassword string `json:"rePassword" binding:"required,eqfield=Password"`  
}  

// use a single instance , it caches struct info  
var (  
    uni        *ut.UniversalTranslator  
    validate   *validator.Validate  
    translator ut.Translator  
)  

func InitTranslate(locale string) (err error) {  
    // Modify the validator engine properties in the gin framework to achieve translation effects  
    engine, ok := binding.Validator.Engine().(*validator.Validate)  
    if !ok {  
        fmt.Println("Error getting gin framework's validator")  
    }  

    // Implement response content tag starting with lowercase  
    engine.RegisterTagNameFunc(func(field reflect.StructField) string {  
        // For example, RePassword string `json:"rePassword" binding:"required,eqfield=Password"`  
        // Then jsonTag is rePassword  
        jsonTag := field.Tag.Get("json")  
        // Split, for example strings.SplitN("a,b,c", ",", 2) gives [a b,c], strings.SplitN("a,b,c,d", ",", 2) gives [a b c,d]  
        splitN := strings.SplitN(jsonTag, ",", 2)  
        name := splitN[0]  
        if name == "-" {  
            return ""  
        }  
        return name  
    })  

    zhT := zh.New()  
    enT := en.New()  

    uni := ut.New(enT, zhT, enT)  

    translator, ok = uni.GetTranslator(locale)  
    if !ok {  
        return fmt.Errorf("uni.GetTranslator(%s)", locale)  
    }  

    switch locale {  
    case "en":  
        en_translations.RegisterDefaultTranslations(engine, translator)  
    case "zh":  
        zh_translations.RegisterDefaultTranslations(engine, translator)  
    }  

    return  
}  

// Remove the struct name from the response, e.g., `SingUpForm.age` becomes `age`  
func removeTopStruct(fields map[string]string) map[string]string {  
    resp := make(map[string]string)  
    for key, value := range fields {  
        resp[key[strings.Index(key, ".")+1:]] = value  
    }  
    return resp  
}  

References#

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.