【Go validator】エラーメッセージのフィールド名をJSONキーに変更+メッセージ内容をカスタマイズ
はじめに
Goでバリデーションを実装する際によく使われるgo-playground/validatorについての記事です。
デフォルトだと、バリデーションエラーが発生した時のメッセージは以下のような形式です。
"Key: 'PostUserParameter.Name' Error:Field validation for 'Name' failed on the 'required' tag"
バリデーションエラーが発生したフィールドの構造体(PostUserParameter
)の名前が表示され、かつJSONのキーではなく構造体フィールド名(Name
)が表示されます。
これをそのままクライアントに返したりするとちょっとイケていないので、カスタマイズする方法を紹介します。
最終的なエラーメッセージは、以下のようになります。
"'name' validation failed on the required tag"
環境
Go: 1.21.3
validator: 10.19.0
STEP0: 今回使用するサンプルコードを紹介
今回使用するサンプルコード(初期状態)です。
/user
というエンドポイントを作成し、POSTメソッドを受け付けるハンドラを実装しております。
リクエストボディはname
とage
(どちらも必須)という構造です。
※コード量を減らすため、所々で返却されるエラーは無視するようにしてます
package main
import (
"encoding/json"
"io"
"net/http"
"github.com/go-playground/validator/v10"
)
type (
// PostUserParameter /userへのPOSTリクエストボディ
PostUserParameter struct {
Name string `json:"name" validate:"required"`
Age int `json:"age" validate:"required"`
}
// ErrorResponse エラー時のレスポンスボディ
ErrorResponse struct {
Message string `json:"message"` // ここにバリデーションエラー時のエラーメッセージを入れる
}
)
func main() {
// ルーティング定義
http.HandleFunc("/user", func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodPost {
// リクエストボディ
reqBuf, _ := io.ReadAll(r.Body)
body := &PostUserParameter{}
_ = json.Unmarshal(reqBuf, body)
// バリデーション
if err := validator.New().Struct(body); err != nil {
// バリデーションエラーが発生した場合は400レスポンスを返却する
errorResponse := ErrorResponse{
Message: err.Error(),
}
resBuf, _ := json.MarshalIndent(errorResponse, "", " ")
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusBadRequest)
_, _ = w.Write(resBuf)
return
}
w.WriteHeader(http.StatusOK)
return
}
})
// サーバー起動
http.ListenAndServe(":8080", nil)
}
こちらをgo run
コマンド等を実行しサーバーを起動した後、下記のname
プロパティが欠損しているcurlコマンドを実行すると、Key: 'PostUserParameter.Name' Error:Field validation for 'Name' failed on the 'required' tag
というメッセージが返却されます。
curl 'http://localhost:8080/user' \
-H 'Content-Type: application/json' \
-w '\nhttp_code: %{http_code}\n' \
-d '{
"age": 20
}'
この状態から、下記の2STEPに分けてエラーメッセージを改良していきます。
STEP1: 表示するフィールド名をJSONキー名に変更する
validatorパッケージに用意されているRegisterTagNameFunc
を用いて、表示するフィールド名をJSONのキー名に変更する実装を行います。
// ...変更無し部分は省略
// validatorインスタンスを作成
validation := validator.New()
// フィールド名にjsonタグの値を設定する
validation.RegisterTagNameFunc(func(field reflect.StructField) string {
// jsonタグで指定されている最初の要素のみを取得する(omitempty等のカンマ以降の値は切り捨てる)
jsonKey := strings.SplitN(field.Tag.Get("json"), ",", 2)[0]
if jsonKey != "" && jsonKey != "-" {
return jsonKey
}
// jsonタグが指定されていない要素はデフォルトの構造体フィールド名を設定する
return field.Name
})
if err := validation.Struct(body); err != nil {
// ...省略
}
// ...変更無し部分は省略
修正後に、上と同じcurlコマンドを実行すると、Key: 'PostUserParameter.name' Error:Field validation for 'name' failed on the 'required' tag
というエラーメッセージが返却されます。
Name
という構造体フィールド名だったものが、name
というJSONキー名になってますね!
ただ、冒頭に構造体自体の名前(PostUserParameter.name
)が入ってしまっているので、次のステップでこちらを修正していきます。
参考:
この時点でのコード全文はこちら
package main
import (
"encoding/json"
"io"
"net/http"
"reflect"
"strings"
"github.com/go-playground/validator/v10"
)
type (
// PostUserParameter /userへのPOSTリクエストボディ
PostUserParameter struct {
Name string `json:"name" validate:"required"`
Age int `json:"age" validate:"required"`
}
// ErrorResponse エラー時のレスポンスボディ
ErrorResponse struct {
Message string `json:"message"` // ここにバリデーションエラー時のエラーメッセージを入れる
}
)
func main() {
// ルーティング定義
http.HandleFunc("/user", func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodPost {
// リクエストボディ
reqBuf, _ := io.ReadAll(r.Body)
body := &PostUserParameter{}
_ = json.Unmarshal(reqBuf, body)
// validatorインスタンスを作成
validation := validator.New()
// フィールド名にjsonタグの値を設定する
validation.RegisterTagNameFunc(func(field reflect.StructField) string {
// jsonタグで指定されている最初の要素のみを取得する(omitempty等のカンマ以降の値は切り捨てる)
jsonKey := strings.SplitN(field.Tag.Get("json"), ",", 2)[0]
if jsonKey != "" && jsonKey != "-" {
return jsonKey
}
// jsonタグが指定されていない要素はデフォルトの構造体フィールド名を設定する
return field.Name
})
if err := validation.Struct(body); err != nil {
// バリデーションエラーが発生した場合は400レスポンスを返却する
errorResponse := ErrorResponse{
Message: err.Error(),
}
resBuf, _ := json.MarshalIndent(errorResponse, "", " ")
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusBadRequest)
_, _ = w.Write(resBuf)
return
}
w.WriteHeader(http.StatusOK)
return
}
})
// サーバー起動
http.ListenAndServe(":8080", nil)
}
STEP2: エラーメッセージをカスタマイズする
このSTEPでは、エラーメッセージKey: 'PostUserParameter.name' Error:Field validation for 'name' failed on the 'required' tag
から、構造体名(PostUserParameter
)を削除し、良い感じにエラーメッセージをカスタマイズしていきます。
validatorには、バリデーションエラー時に発生する型としてValidationErrors
が用意されているので、型をチェックし、バリデーションエラーの場合はメッセージをカスタマイズします。
詳細はコードコメントを参照ください。
// ...変更無し部分は省略
if err := validation.Struct(body); err != nil {
var errorMessage string
// 発生したエラーがValidationErrorsであるかをチェックする
if verrs, ok := err.(validator.ValidationErrors); ok {
var msgs []string
// ValidationErrorsは、複数フィールド分のエラーが格納されている(FieldErrorのスライス型)であるため、ループしてメッセージを整形する
for _, ferr := range verrs {
// Namespaceに'PostUserParameter.name'のような「構造体名.JSONキー」が格納されている
// 構造体名を取り除くため「.」のインデックスを特定し、トリムしている
i := strings.Index(ferr.Namespace(), ".")
ns := ferr.Namespace()[i+1:]
// msgsというエラーメッセージを格納するスライスに整形した文字を追加する
// ferr.Tag()は、エラーとなった対象のvalidateタグ(required等)が入っている
msgs = append(msgs, fmt.Sprintf("'%s' validation failed on the %s tag", ns, ferr.Tag()))
}
errorMessage = fmt.Sprint(strings.Join(msgs, "\n"))
} else {
errorMessage = err.Error()
}
// ...変更無し部分は省略
再度、上と同じcurlコマンドを実行すると'name' validation failed on the required tag
というような形で、無事にエラーメッセージから構造体フィールド名をJSONのキーに変更し、構造体名を完全に削除することができました。
コード全文はこちら
package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
"reflect"
"strings"
"github.com/go-playground/validator/v10"
)
type (
// PostUserParameter /userへのPOSTリクエストボディ
PostUserParameter struct {
Name string `json:"name" validate:"required"`
Age int `json:"age" validate:"required"`
}
// ErrorResponse エラー時のレスポンスボディ
ErrorResponse struct {
Message string `json:"message"` // ここにバリデーションエラー時のエラーメッセージを入れる
}
)
func main() {
// ルーティング定義
http.HandleFunc("/user", func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodPost {
// リクエストボディ
reqBuf, _ := io.ReadAll(r.Body)
body := &PostUserParameter{}
_ = json.Unmarshal(reqBuf, body)
// validatorインスタンスを作成
validation := validator.New()
// フィールド名にjsonタグの値を設定する
validation.RegisterTagNameFunc(func(field reflect.StructField) string {
// jsonタグで指定されている最初の要素のみを取得する(omitempty等のカンマ以降の値は切り捨てる)
jsonKey := strings.SplitN(field.Tag.Get("json"), ",", 2)[0]
if jsonKey != "" && jsonKey != "-" {
return jsonKey
}
// jsonタグが指定されていない要素はデフォルトの構造体フィールド名を設定する
return field.Name
})
if err := validation.Struct(body); err != nil {
var errorMessage string
// 発生したエラーがValidationErrorsであるかをチェックする
if verrs, ok := err.(validator.ValidationErrors); ok {
var msgs []string
// ValidationErrorsは、複数フィールド分のエラーが格納されている(FieldErrorのスライス型)であるため、ループしてメッセージを整形する
for _, ferr := range verrs {
// Namespaceに'PostUserParameter.name'のような「構造体名.JSONキー」が格納されている
// 構造体名を取り除くため「.」のインデックスを特定し、トリムしている
i := strings.Index(ferr.Namespace(), ".")
ns := ferr.Namespace()[i+1:]
// msgsというエラーメッセージを格納するスライスに整形した文字を追加する
// ferr.Tag()は、エラーとなった対象のvalidateタグ(required等)が入っている
msgs = append(msgs, fmt.Sprintf("'%s' validation failed on the %s tag", ns, ferr.Tag()))
}
errorMessage = fmt.Sprint(strings.Join(msgs, "\n"))
} else {
errorMessage = err.Error()
}
// バリデーションエラーが発生した場合は400レスポンスを返却する
errorResponse := ErrorResponse{
Message: errorMessage,
}
resBuf, _ := json.MarshalIndent(errorResponse, "", " ")
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusBadRequest)
_, _ = w.Write(resBuf)
return
}
w.WriteHeader(http.StatusOK)
return
}
})
// サーバー起動
http.ListenAndServe(":8080", nil)
}
終わりに
もっと簡単にカスタマイズできても良いのに..笑
最後まで読んでいただき、ありがとうございました。
Discussion