Yappli Tech Blog

株式会社ヤプリの開発メンバーによるブログです。最新の技術情報からチーム・働き方に関するテーマまで、日々の熱い想いを持って発信していきます。

Goのgo-playground/validateパッケージを理解する

はじめに

こんにちは、サーバーサイドエンジニアの中川(tkdev0728)です。
最近タイピング中の手首の疲れが気になりパームレストを導入しました。正直言って元々パームレストに対して否定的だったのですが、 ある日手首の位置が落ち着かず半信半疑で導入してみると快適さに気づき、もっと早く使っていればよかったと思っています。

さて、今回はGoでバリデーション処理を行いたく、ヤプリの他の処理でも使われているvalidateパッケージを使おうと思いました。
その際に自分がvalidateパッケージについて詳しくなく、やりたいことを実現するまでにいろいろと調べたりしたので、それらについて書いていこうと思います。 自分のように詳しく知りたい人、正直言ってなんとなく使っている人の参考になれば幸いです。

使い方

基本中の基本

何はともあれまずはインストールです

$ go get github.com/go-playground/validator/v10

インストールできたらサンプルコードで試してみます。
Userに対してのバリデーションを行う処理ですが、誤って必須フィールドのEmailフィールドをコメントアウトしています。

package main

import (
    "fmt"

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

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

func main() {
    user := User{
        Name: "John",
        // Email: "john@example.com",
        Age: 130,
    }

    validate := validator.New()
    err := validate.Struct(user)
    if err != nil {
        for _, err := range err.(validator.ValidationErrors) {
            fmt.Println(err.Field(), err.Tag(), err.Param())
        }
    } else {
        fmt.Println("Validation successful!")
    }
}

実行すると以下の通りです。必須フィールドのEmailがコメントアウトされてしまっているのでバリデーションエラーが返ってくることが確認できると思います。
基本中の基本の使い方としてはこのような感じです。

$ go run main.go
Email required 

カスタムバリデーション

基本中の基本の例ではパッケージ内で宣言されているデフォルトのルールを使ってバリデーションを行っていました。
デフォルトのルールは以下にまとまっているのですが、結構色々あり活用できそうなのでぜひ一度目を通しておくといいかと思います。

github.com

そんな既存のルールだけでなく、自分でバリデーション用の関数を作成してバリデーションを行うカスタムバリデーションも実現できます。 カスタムバリデーションは単一フィールドの値をチェックするFieldLevelのバリデーションと、構造体全体をチェックし複数フィールドの関係や依存会計を検証するStructLevelのバリデーションを設定できます。

FieldLevelの使い方

以下にFieldLevelのバリデーションのサンプルコードを記載しています。
emailのバリデーションについてデフォルトのルールではなく、独自の正規表現を用いたバリデーションルールを設定しています。
コード中のコメントにも記載していますが、validate.RegisterValidation() でバリデーションルールを登録する際、emailなどすでに存在しているキーを設定すると上書きされることに注意が必要です。

package main

import (
    "fmt"
    "regexp"

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

var validate *validator.Validate

type User struct {
    Name  string `validate:"required"`
    Email string `validate:"required,email"`
}

func validateEmail(fl validator.FieldLevel) bool {
    email := fl.Field().String()

    // 簡易的なメールアドレスの形式チェック
    emailRegex := regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`)
    return emailRegex.MatchString(email)
}

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

    // ここで指定したキーがすでに存在していた場合新しいルールが使われるようになるので、デフォルトのemailのバリデーションではなくカスタムバリデーションが使われます
    validate.RegisterValidation("email", validateEmail)

    user := User{Name: "John Doe", Email: "john.doe@example.com"}

    err := validate.Struct(user)
    if err != nil {
        validationErrors := err.(validator.ValidationErrors)
        for _, e := range validationErrors {
            fmt.Printf("Validation failed on field '%s' with tag '%s'\n", e.Field(), e.Tag())
        }
    } else {
        fmt.Println("Validation successful!")
    }
}

実行結果

$ go run main.go
Validation failed on field 'Email' with tag 'email'

StructLevelの使い方

同様にStructLevelのバリデーションのサンプルコードを記載しています。
User構造体のPasswordフィールドとConfirmFieldの値が一致しているかを検証しています。
FieldLevelとの違いはバリデーション関数の引数の型がvalidator.StructLevelになっている点です。

package main

import (
    "fmt"

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

var validate *validator.Validate

type User struct {
    Username        string `validate:"required"`
    Password        string `validate:"required"`
    ConfirmPassword string `validate:"required"`
}

func validateUserStruct(sl validator.StructLevel) {
    user := sl.Current().Interface().(User)

    if user.Password != user.ConfirmPassword {
        sl.ReportError(user.ConfirmPassword, "ConfirmPassword", "ConfirmPassword", "eqfield", "Password")
    }
}

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

    validate.RegisterStructValidation(validateUserStruct, User{})

    user := User{
        Username:        "john_doe",
        Password:        "123456",
        ConfirmPassword: "654321",
    }

    err := validate.Struct(user)
    if err != nil {
        validationErrors := err.(validator.ValidationErrors)
        for _, e := range validationErrors {
            fmt.Printf("Validation failed on field '%s' with tag '%s'\n", e.Field(), e.Tag())
        }
    } else {
        fmt.Println("Validation successful!")
    }
}

実行結果

$ go run main.go
Validation failed on field 'ConfirmPassword' with tag 'eqfield'

やりたかったユースケース

基本的な使い方がわかったところでここからは私が実際にやりたかった内容を紹介していきます。
実際には紹介する内容よりも複雑でしたが、噛み砕いていくと以下の2つだったので以下の2つを紹介します。

スライスに対してのバリデーション

ここまでは1階層のみのシンプルな構造体に対してのバリデーションについて紹介してきました。
実際には入れ子構造になったりスライスやマップを含んだ複雑な構造体に対してバリデーションを行いたいケースが多いと思います。もちろんそう言ったケースにも対応した書き方はあり、構造体のvalidateタグに dive オプションを追加すると子階層のフィールドに対してバリデーションができます。ちなみにdiveオプションはスライスやマップの要素に対して再帰的にバリデーションを適用するためのオプションであり、単なるネストされた構造体の場合はdiveオプションを指定する必要はありません。

package main

import (
    "fmt"

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

var validate *validator.Validate

type Skill struct {
    Name            string `validate:"required"`
    ExperienceYears int    `validate:"gte=1"`
}

type User struct {
    Username string  `validate:"required"`
    Email    string  `validate:"required,email"`
    Age      int     `validate:"gte=0,lte=130"`
    Skills   []Skill `validate:"required,dive"`
}

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

    user := User{
        Username: "john_doe",
        Email:    "john.doe@example.com",
        Age:      30,
        Skills: []Skill{
            {
                Name:            "Go Programming",
                ExperienceYears: 3,
            },
            {
                Name:            "",
                ExperienceYears: 1,
            },
            {
                Name:            "Python",
                ExperienceYears: 0,
            },
        },
    }

    err := validate.Struct(user)
    if err != nil {
        validationErrors := err.(validator.ValidationErrors)
        for _, e := range validationErrors {
            fmt.Printf("Validation failed on field '%s' with tag '%s'\n", e.Namespace(), e.Tag())
        }
    } else {
        fmt.Println("Validation successful!")
    }
}

実行結果

$ go run main.go
Validation failed on field 'User.Skills[1].Name' with tag 'required'
Validation failed on field 'User.Skills[2].ExperienceYears' with tag 'gte'

複数の構造体に対してのバリデーション

構造体Aのフィールドと構造体Bのフィールドを比較したく、どうしようかと思っていたのですが考え方としては構造体AとBを含んだ1つの構造体を用意し、その構造体に対してStructLevelで比較すればいいだけでした。

package main

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

var validate *validator.Validate

type Address struct {
    Street string `validate:"required"`
    City   string `validate:"required"`
}

type User struct {
    Username        string  `validate:"required"`
    PrimaryAddress  Address `validate:"required"`
    SecondaryAddress Address `validate:"required"`
}

func StructLevelValidation(sl validator.StructLevel) {
    user := sl.Current().Interface().(User)

    if user.PrimaryAddress == user.SecondaryAddress {
        sl.ReportError(user.SecondaryAddress, "SecondaryAddress", "SecondaryAddress", "eqfield", "")
    }
}

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

    validate.RegisterStructValidation(StructLevelValidation, User{})

    user := User{
        Username: "john_doe",
        PrimaryAddress: Address{
            Street: "123 Main St",
            City:   "Hometown",
        },
        SecondaryAddress: Address{
            Street: "123 Main St",
            City:   "Hometown",
        },
    }

    err := validate.Struct(user)
    if err != nil {
        validationErrors := err.(validator.ValidationErrors)
        for _, e := range validationErrors {
            fmt.Printf("Validation failed on field '%s' with tag '%s'\n", e.Namespace(), e.Tag())
        }
    } else {
        fmt.Println("Validation successful!")
    }
}

実行結果

$ go run main.go
Validation failed on field 'User.SecondaryAddress' with tag 'eqfield'

感想

デフォルトで用意されているバリデーション処理と、必要に応じてカスタムバリデーションを設定できること、カスタムバリデーションはFieldLevelとStructLevelが設定できることが理解できました。
バリデーションを行うユースケースは多いと思いますが、デフォルトのルールをうまく使いつつ、固有のドメインに依存するルールはカスタムバリデーションを使うなど、うまく使いわけしていきたいですね!

参考文献

https://pkg.go.dev/github.com/go-playground/validator/v10@v10.22.0#section-readme