理系学生日記

おまえはいつまで学生気分なのか

go-playground/validatorでYAMLファイルのバリデーションを行い、エラーのあったフィールドを表示する

Golangでの有名バリデーションライブラリにgo-playground/validatorというのがあります。ライセンスはMITで使いやすい。

このライブラリを使ってYAMLファイルのバリデーションを実装したのですが、そこそこ悩まされました。

本ライブラリはYAML等のバリデーションに特化したものではなく、汎用のバリデーションライブラリです。 結果として、根底的な思想はStructに対するバリデーションであり、「どこでエラーが発生したのか」はフィールド名で返却されます。

package validator

import (
    "testing"

    pgvalidator "github.com/go-playground/validator/v10"
    "github.com/stretchr/testify/assert"
)

type Parent struct {
    Name     string   `yaml:"name" validate:"required"`
    Children []*Child `yaml:"children" validate:"dive,required"`
}

type Child struct {
    Name string `yaml:"child_name,fuga,piyo" validate:"required"`
    Age  int    `yaml:"child_age,fuga,piyo" validate:"required,gte=0"`
}

func TestField(t *testing.T) {
    p := &Parent{
        Name: "Parent",
        Children: []*Child{
            {Name: "Taro", Age: 10},
            {Name: "Jiro", Age: -5},
        },
    }

    if err := pgvalidator.New().Struct(p); err != nil {
        if assert.Error(t, err) {
            for _, ve := range err.(pgvalidator.ValidationErrors) {
                // "child_age" ではなく "Age" で返却される
                assert.Equal(t, "Age", ve.Field())
                assert.Equal(t, "Parent.Children[1].Age", ve.Namespace())
            }
        }
    }
}

Structのバリデーションであればそれで良いのですが、YAMLファイルのバリデーションであればyamlタグから取得したいですよね。

この用途のためには、RegisterTagNameFuncを使います。この関数にはStructFieldが渡ってきますので、そこからyamlタグの内容を取り出せば良い。

    v.RegisterTagNameFunc(func(f reflect.StructField) string {
        name := strings.SplitN(f.Tag.Get("yaml"), ",", 2)[0]
        if name == "-" {
            return ""
        }
        return name
    })

実際にこれを挟むと、以下のようなテストコードがパスするようになります。

func TestField(t *testing.T) {
    p := &Parent{
        Name: "Parent",
        Children: []*Child{
            {Name: "Taro", Age: 10},
            {Name: "Jiro", Age: -5},
        },
    }

    v := pgvalidator.New()
    v.RegisterTagNameFunc(func(f reflect.StructField) string {
        name := strings.SplitN(f.Tag.Get("yaml"), ",", 2)[0]
        if name == "-" {
            return ""
        }
        return name
    })

    if err := v.Struct(p); err != nil {
        if assert.Error(t, err) {
            for _, ve := range err.(pgvalidator.ValidationErrors) {
                assert.Equal(t, "child_age", ve.Field())
                assert.Equal(t, "Parent.children[1].child_age", ve.Namespace())
            }
        }
    }
}

バリデーションエラーとなった項目は、YAMLファイルのルートからのパスで特定したいのでNamespace()を使いたい。一方で気になるのは最後のve.Namespace()Parentというstruct名が返却されていることですね。 YAMLのルート要素となるStruct名には興味がありませんので、以下のようなコードで最初のStruct名を省きます。

    field := strings.SplitN(ve.Namespace(), ".", 2)[1]

というわけで、最終的にはこんなコードを使うことにしました。

func TestField(t *testing.T) {
    p := &Parent{
        Name: "Parent",
        Children: []*Child{
            {Name: "Taro", Age: 10},
            {Name: "Jiro", Age: -5},
        },
    }

    v := pgvalidator.New()
    v.RegisterTagNameFunc(func(f reflect.StructField) string {
        name := strings.SplitN(f.Tag.Get("yaml"), ",", 2)[0]
        if name == "-" {
            return ""
        }
        return name
    })

    if err := v.Struct(p); err != nil {
        if assert.Error(t, err) {
            for _, ve := range err.(pgvalidator.ValidationErrors) {
                assert.Equal(t, "child_age", ve.Field())
                assert.Equal(t, "children[1].child_age", strings.SplitN(ve.Namespace(), ".", 2)[1])
            }
        }
    }
}