banner
biuaxia

biuaxia

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

定制GinのValidatorエラーメッセージとレスポンス構造

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

Snipaste20211114190832.png

エラーメッセージのローカライズ#

直接コードを見てみましょう。ロジックは非常に明確です。エラーメッセージが発生するたびに、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
}

参考資料#

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。