【Go】validatorのタグでカスタムメッセージを設定する

# はじめに
バリデーションライブラリである`go-playground/validator`は非常に強力ですが、デフォルトのエラーメッセージをフィールドごとにカスタマイズしたい場合があります。そこで、今回はカスタムメッセージをタグで設定できるようにするライブラリ`cmvalidator`を作成したので紹介します。

https://github.com/go-playground/validator

# デフォルトのバリデーションメッセージの課題
通常、`go-playground/validator`を使用して構造体をバリデートする際には、次のようなデフォルトのエラーメッセージが生成されます。
```go
package main

import (
    "github.com/go-playground/validator/v10"
)

type User struct {
    FirstName string `validate:"required"`
    LastName  string `validate:"required"`
    Age       int    `validate:"gte=0,lte=130"`
    Email     string `validate:"required,email"`
}

func main() {
    validate := validator.New()

    newUser := User{
        FirstName: "",
        LastName:  "",
        Age:       200,
        Email:     "invalid-email",
    }

    err := validate.Struct(newUser)
    if err != nil {
        for _, err := range err.(validator.ValidationErrors) {
            println("Validation error:", err.Error())
        }
    }
}
```
このコードでは、以下のようなエラーメッセージが表示されます。
```bash
Validation error: Key: 'User.FirstName' Error:Field validation for 'FirstName' failed on the 'required' tag
Validation error: Key: 'User.LastName' Error:Field validation for 'LastName' failed on the 'required' tag
Validation error: Key: 'User.Age' Error:Field validation for 'Age' failed on the 'lte' tag
Validation error: Key: 'User.Email' Error:Field validation for 'Email' failed on the 'email' tag
```
これらのメッセージは技術者にとっては十分な情報ですが、エンドユーザーに直接提示する場合、もう少し分かりやすいメッセージが欲しいです。またフィールドごとにカスタマイズしたメッセージが欲しい場合、開発者がFieldErrorインターフェースを使ってエラーメッセージを変換する必要があります。

https://github.com/go-playground/validator/issues/559

# cmvalidatorを使ったカスタムメッセージの設定
そこで今回作成した`cmvalidator`というライブラリを使うことで、構造体のタグにカスタムメッセージを設定でき、ユーザーにとって分かりやすいメッセージを表示することができます。

https://github.com/yudai2929/cmvalidator

## インストール
まず、`cmvalidator`をプロジェクトにインストールします。
```bash
go get github.com/yudai2929/cmvalidator
```

## 使用方法

次に、以下のようにカスタムメッセージを含む構造体を定義します。

```go
package main

import (
    "github.com/yudai2929/cmvalidator"
)

type User struct {
    FirstName string `validate:"required" customMessage:"名前を入力してください！"`
    LastName  string `validate:"required" customMessage:"姓を入力してください！"`
    Age       int    `validate:"gte=0,lte=130" customMessage:"年齢は0から130の間で入力してください！"`
    Email     string `validate:"required,email" customMessage:"有効なメールアドレスを入力してください！"`
}
```

次に、`cmvalidator`を使ってバリデーションを行います。
```go
package main

import (
    "errors"
    "github.com/yudai2929/cmvalidator"
)

func main() {
    validate := cmvalidator.New()

    newUser := User{
        FirstName: "",
        LastName:  "",
        Age:       200,
        Email:     "invalid-email",
    }

    err := validate.Struct(newUser)
    if err != nil {
        var cmvalidateErrors cmvalidator.CFValidateErrors
        if errors.As(err, &cmvalidateErrors) {
            for _, cmvalidateError := range cmvalidateErrors {
                println("Custom Message:", cmvalidateError.CustomMessage())
            }
        }
    }
}
```

このコードを実行すると、次のようにカスタムメッセージが出力されます。
```bash
Custom Message: 名前を入力してください！
Custom Message: 姓を入力してください！
Custom Message: 年齢は0から130の間で入力してください！
Custom Message: 有効なメールアドレスを入力してください！
```

# 内部挙動の解説
`cmvalidator`の内部では、`go-playground/validator`のバリデーション機能を拡張する形で動作しています。

## カスタムバリデーターの作成
`cmvalidator`は`go-playground/validator`をラップした`CMValidate`という構造体を提供します。この構造体は、`Struct`および`StructCtx`メソッドを持ち、これらを通じてバリデーション処理を行います。
```go
type CMValidate struct {
    *validator.Validate
}

func New() *CMValidate {
    return &CMValidate{validator.New()}
}
```

## バリデーションエラーの変換
`Struct`メソッドや`StructCtx`メソッドでは、まず通常のバリデーションを行い、その結果を元にエラーメッセージを生成します。エラーメッセージは、構造体のフィールドタグから`customMessage`タグを取得し、それを適用する形でカスタマイズされます。
```go

func (cv *CMValidate) Struct(s any) (err error) {
	return cv.StructCtx(context.Background(), s)
}

func (cv *CMValidate) StructCtx(ctx context.Context, s any) (err error) {
	err = cv.Validate.StructCtx(ctx, s)

	if err == nil {
		return
	}

	var validationErrors validator.ValidationErrors
	if !errors.As(err, &validationErrors) {
		return
	}

	return cnvToCMValidateErrors(validationErrors, s)
}

func cnvToCMValidateErrors(err validator.ValidationErrors, s any) CFValidateErrors {
    cmvalidateErrors := make(CFValidateErrors, 0, len(err))
    for _, validationError := range err {
        cmvalidateErrors = append(cmvalidateErrors, cnvToCMFieldError(validationError, s))
    }
    return cmvalidateErrors
}

func cnvToCMFieldError(err validator.FieldError, s any) CMFieldError {
    typ := reflect.TypeOf(s)
    fieldName := err.StructField()

    field, ok := typ.FieldByName(fieldName)
    if !ok {
        return cmFieldError{
            FieldError:    err,
            customMessage: "",
        }
    }

    customMessage := field.Tag.Get(CustomMessageTag)
    return cmFieldError{
        FieldError:    err,
        customMessage: customMessage,
    }
}
```
## エラーのラッピング
最後に、カスタムメッセージを含むエラー情報を`CMFieldError`としてラップし、これを返却します。このカスタムエラーメッセージは、後で`CustomMessage`メソッドを使って簡単に取得することができます。
```go
type CMFieldError interface {
    validator.FieldError
    CustomMessage() string
}

type cmFieldError struct {
    validator.FieldError
    customMessage string
}

func (cfe cmFieldError) CustomMessage() string {
    return cfe.customMessage
}
```

# cmvalidatorの課題
`cmvalidator`では、フィールドごとにカスタムメッセージを設定できますが、特定のバリデーションタグ（例えば`required`や`email`など）に対して共通のメッセージを指定する機能はありません。現状では、フィールドごとにカスタムメッセージを設定する必要があるため、同じバリデーションルールに対しても各フィールドに個別にメッセージを設定しなければなりません。

# まとめ
`cmvalidator`を使うことで、構造体のバリデーションエラーが起きたフィールドに対して、より詳細なカスタムメッセージを簡単に設定することができます。興味のある方はぜひ試してみてください。

https://github.com/yudai2929/cmvalidator