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
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
}