validator.v9(v10) で条件付き必須バリデーションする


はじめに

Goのバリデーション用パッケージのvalidator.v9を使って、条件付き必須のバリデーションを実装したかったのですが、これまでrequired, min, maxくらいしか使っていなかったので、勉強しなおしました。
ちなみにvalidator.v10でも同じ模様です。

公式ドキュメントに丁寧に書いてあるのですが、日本語でまとめられているものが無さそうなので書いておきます。

validator.v9の使い方

この記事を読んでいる人であればおそらく説明の必要もないと思いますが、一応。
公式サンプルをチョットいじっています。

package main

import (
    "fmt"

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

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

var validate *validator.Validate

func main() {

    validate = validator.New()

    user := &User{
        Name:      "Badger",
        Age:       135,
    }

    err := validate.Struct(user)
    if err != nil {
        if _, ok := err.(*validator.InvalidValidationError); ok {
            fmt.Println(err)
            return
        }

        for _, err := range err.(validator.ValidationErrors) {

            fmt.Println(err.Namespace())
            fmt.Println(err.Field())
            fmt.Println(err.StructNamespace())
            fmt.Println(err.StructField())
            fmt.Println(err.Tag())
            fmt.Println(err.ActualTag())
            fmt.Println(err.Kind())
            fmt.Println(err.Type())
            fmt.Println(err.Value())
            fmt.Println(err.Param())
            fmt.Println()
        }
        // バリデーションエラーの場合の処理
        return
    }

    // バリデーションを通った場合の処理}

ちなみに、こんな感じに構造全体ではなく、単一の項目に対してのバリデーションもできるようです。

errs := validate.Var(myEmail, "required,email")

条件付き必須バリデーションの実現方法

ここで言う条件付き必須とは「◯◯が××のとき、△△は必須である」を必須確認処理を行うバリデーションを指します。

条件付き必須バリデーションを実現する方式としては大きく2つ存在します。

  • Validator Tagsによる条件付き必須
  • Custom Validation Functionsによる条件付き必須

それぞれについて見ていきます。

Validator Tagsによる条件付き必須

そもそもValidator Tagsによって条件付き必須処理は可能です。
ただし、その条件に制限があるため、求める条件処理となっていない場合はこの方法が使えません。

利用可能なTagsは下記の4つです。

  • required_with
  • required_with_all
  • required_without
  • required_without_all

Required With

required_withタグは、「◯◯が存在する場合、△△は必須である」というバリデーションを行います。
スペース区切りで存在する対象の条件を増やすこともできます。
複数の条件を指定する場合は、対象の内一つでも存在すれば必須確認を行います。

// Field1が存在する場合は、Usageは必須である
Usage: required_with=Field1

// Field1 もしくは Field2 のどちらかが存在する場合は、 Usage は必須である。
Usage: required_with=Field1 Field2

Required With All

required_with_allタグは、「対象とするフィールド全てが存在する場合、△△は必須である」というバリデーションを行います。

// Field1 と Field2 の両方が存在する場合、 Usage は必須である。
Usage: required_with_all=Field1 Field2

Required Without

上記の「◯◯が存在する場合」とは逆に「◯◯が存在しない場合」の条件付き必須Tagsもあります。

required_withoutは「◯◯が存在しない場合、△△は必須である」というバリデーションを行います。
スペース区切りで存在する対象の条件を増やすこともできます。
複数の条件を指定する場合は、対象の内一つでも存在しなければ必須確認を行います。

// Field1が存在しない場合は、Usageは必須である
Usage: required_without=Field1

// Field1 もしくは Field2 のどちらかが存在しない場合は、 Usage は必須である。
Usage: required_without=Field1 Field2

Required Without All

こちらも上記の「◯◯が全て存在する場合」とは逆に「◯◯が全て存在しない場合」の条件付き必須Tagsもあります。

required_without_allは、「対象とするフィールド全てが存在しない場合、△△は必須である」というバリデーションを行います。

// Field1 と Field2 の両方が存在しない場合、 Usage は必須である。
Usage: required_without_all=Field1 Field2

Custom Validation Functionsによる条件付き必須

上記のrequired系のValidator Tagsでは、対象のフィールドの存在有無のみによって必須確認をするかどうかが決まります。
対象フィールドの値や構造に応じての条件付き必須処理を行うには別の方法を取る必要があるようです。

その方法がCustom Validation Functionsの利用です。
下記の例では名前がtaroの場合、年齢の入力を必須にしています。
世の太郎さんには伏して謝罪いたします。

package main

import (
    "fmt"

    validator "gopkg.in/go-playground/validator.v9"
)

type User struct {
    Name string `json:"name"`
    Age  uint8  `validate:"gte=5,lte=130"`
}

var validate *validator.Validate

func main() {

    validate = validator.New()

    validate.RegisterStructValidation(TaroRequiredAgeValidation, User{})

    user := &User{
        Name: "taro",
        // Age:  ,
    }

    err := validate.Struct(user)
    if err != nil {
        if _, ok := err.(*validator.InvalidValidationError); ok {
            fmt.Println(err)
            return
        }

        for _, err := range err.(validator.ValidationErrors) {

            fmt.Println(err.Namespace())
            fmt.Println(err.Field())
            fmt.Println(err.StructNamespace())
            fmt.Println(err.StructField())
            fmt.Println(err.Tag())
            fmt.Println(err.ActualTag())
            fmt.Println(err.Kind())
            fmt.Println(err.Type())
            fmt.Println(err.Value())
            fmt.Println(err.Param())
            fmt.Println()
        }
        // バリデーションエラーの場合の処理
        return
    }

    // バリデーションを通った場合の処理
}

// TaroRequiredAgeValidation Custom Validation Functionsの定義
func TaroRequiredAgeValidation(sl validator.StructLevel) {

    user := sl.Current().Interface().(User)

    if user.Name == "taro" && user.Age == 0 {
        sl.ReportError(user.Age, "Age", "age", "taro_required_age", "")
    }
}

まとめ

対象テーブルの存在有無による必須チェックであればValidation Tagsで、対象テーブルの値や構造まで見ての必須チェックであればCustom Validation Functionsを使えばよいのかなと思います。
ただし、このパッケージに依存することなく、素直にビジネスロジック内での処理でも良いのかなって思ったり。

参考

公式ドキュメント:package validator
https://godoc.org/gopkg.in/go-playground/validator.v9

公式サンプル集
https://github.com/go-playground/validator/tree/master/_examples

【Golang】echoとvalidator.v9を使ったカスタムバリデーションのサンプル
https://qiita.com/nanamen/items/c824a2c8f2e1767f90f8