title: カスタム Gin の Validator エラーメッセージとレスポンス構造
date: 2021-11-14 19:02:00
toc: true
category:
- Golang
- Gin
tags: - Golang
- golang
- Go
- go
- Gin
- gin
- Validator
- validator
- 機能
- 検証
- カスタム
- 実装
- レスポンス
- 構造
- エラー
- メッセージ
- ローカライズ
- 翻訳
前言#
Golang の Validator(検証)機能を使用していると、多くのエラーメッセージが英語で表示されることに気づくでしょう。例えば
{
"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"
}
}
これにより、API の呼び出し者が読みづらくなります。
公式ドキュメントを確認したところ、翻訳作業はすでに行われており、エラーメッセージを指定された言語に変更することができます。
ただし、現在 go-playground/validator
は 13 種類の言語のみをサポートしているため、具体的には公式にリストされたディレクトリを参照してください:validator/translations at master · go-playground/validator
エラーメッセージのローカライズ#
直接コードを見てみましょう。ロジックは非常に明確です。エラーメッセージが発生するたびに、err.(validator.ValidationErrors)
を使って例外オブジェクト validator.ValidationErrors
に変換し、その func (ve ValidationErrors) Translate(ut ut.Translator) ValidationErrorsTranslations {}
メソッドを呼び出すだけです。
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"
)
// 検証メッセージの翻訳
func main() {
if err := InitTranslate("zh"); err != nil {
fmt.Println("翻訳器の初期化エラー")
return
}
r := gin.Default()
r.POST("/", func(c *gin.Context) {
var singUpForm SingUpForm
if err := c.ShouldBind(&singUpForm); nil != err {
// エラーが発生
errors, ok := err.(validator.ValidationErrors)
if !ok {
c.JSON(http.StatusOK, gin.H{
"code": -1,
"msg": fmt.Errorf("エラーメッセージの翻訳取得エラー, %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": "ログイン成功",
})
})
// 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"`
}
// 単一インスタンスを使用し、構造体情報をキャッシュ
var (
uni *ut.UniversalTranslator
validate *validator.Validate
translator ut.Translator
)
func InitTranslate(locale string) (err error) {
// ginフレームワークのvalidatorエンジンプロパティを変更し、翻訳効果を実現
engine, ok := binding.Validator.Engine().(*validator.Validate)
if !ok {
fmt.Println("ginフレームワークの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
}
修正後の効果は次の通りです:
{
"code": -1,
"msg": {
"SingUpForm.Age": "Ageは1以上でなければなりません",
"SingUpForm.Email": "Emailは必須フィールドです",
"SingUpForm.Name": "Nameは必須フィールドです",
"SingUpForm.Password": "Passwordは必須フィールドです",
"SingUpForm.RePassword": "RePasswordは必須フィールドです"
}
}
レスポンス内容構造の属性名の先頭を小文字にする#
元のレスポンス内容:
{
"code": -1,
"msg": {
"SingUpForm.Age": "Ageは1以上でなければなりません",
"SingUpForm.Email": "Emailは必須フィールドです",
"SingUpForm.Name": "Nameは必須フィールドです",
"SingUpForm.Password": "Passwordは必須フィールドです",
"SingUpForm.RePassword": "RePasswordは必須フィールドです"
}
}
修正後の内容は:
{
"code": -1,
"msg": {
"SingUpForm.age": "ageは1以上でなければなりません",
"SingUpForm.email": "emailは必須フィールドです",
"SingUpForm.name": "nameは必須フィールドです",
"SingUpForm.password": "passwordは必須フィールドです",
"SingUpForm.rePassword": "rePasswordは必須フィールドです"
}
}
完全なコードは:
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"
)
// 検証メッセージの翻訳
func main() {
if err := InitTranslate("zh"); err != nil {
fmt.Println("翻訳器の初期化エラー")
return
}
r := gin.Default()
r.POST("/", func(c *gin.Context) {
var singUpForm SingUpForm
if err := c.ShouldBind(&singUpForm); nil != err {
// エラーが発生
errors, ok := err.(validator.ValidationErrors)
if !ok {
c.JSON(http.StatusOK, gin.H{
"code": -1,
"msg": fmt.Errorf("エラーメッセージの翻訳取得エラー, %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": "ログイン成功",
})
})
// 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"`
}
// 単一インスタンスを使用し、構造体情報をキャッシュ
var (
uni *ut.UniversalTranslator
validate *validator.Validate
translator ut.Translator
)
func InitTranslate(locale string) (err error) {
// ginフレームワークのvalidatorエンジンプロパティを変更し、翻訳効果を実現
engine, ok := binding.Validator.Engine().(*validator.Validate)
if !ok {
fmt.Println("ginフレームワークのvalidator取得エラー")
}
// レスポンス内容のタグの先頭を小文字にする
engine.RegisterTagNameFunc(func(field reflect.StructField) string {
// 例えば RePassword string `json:"rePassword" binding:"required,eqfield=Password"`
// その場合、jsonTagはrePasswordです
jsonTag := field.Tag.Get("json")
// 分割、例えば strings.SplitN("a,b,c", ",", 2) なら [a b,c]、strings.SplitN("a,b,c,d", ",", 2) なら [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
}
// レスポンスのStruct名を削除、例えば `SingUpForm.age` は `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
}