Goのvalidator、定番ですよね。よくある、「複数指定出来る項目があるが、そのうちどれか1つしか指定出来ない」というケースのカスタムバリデーションを作成しました。
使い方
validate:"alternative=hogehoge"
このタグを付与すると、alternativeの値が同じフィールドをどれか1つ指定されていないとバリデーションエラーになります。ただし、同じ構造体内のフィールド間でのみ有効です。
実装&使用例
package main
import (
"fmt"
"log"
"strings"
"github.com/go-playground/validator/v10"
)
type Pet struct {
Cat *Cat `validate:"alternative=type"`
Dog *Dog `validate:"alternative=type"`
Bird *string `validate:"alternative=type"`
}
type Cat struct {
Name string
}
type Dog struct {
Name string
}
func alternative(fl validator.FieldLevel) bool {
parent := fl.Parent()
tagValue := fl.Param()
num := 0
for i := 0; i < parent.NumField(); i++ {
if v, ok := parent.Type().Field(i).Tag.Lookup("validate"); ok {
for _, tag := range strings.Split(v, ",") {
// 同じ値のvalidateタグで、ゼロ値でなければカウントアップ
if tag == "alternative="+tagValue && !parent.Field(i).IsZero() {
num++
break
}
}
}
// 2つ以上あったらfalse
if num > 1 {
return false
}
}
// 1個ならtrue, 0個ならfalse
return num == 1
}
func main() {
validate := validator.New()
err := validate.RegisterValidation("alternative", alternative, true)
if err != nil {
log.Fatalf("failed to register validation. err: %s", err)
}
hasCat := Pet{
Cat: &Cat{"neko"},
}
bird := "bird"
hasBird := Pet{
Bird: &bird,
}
noPet := Pet{}
hasTwo := Pet{
Cat: &Cat{"neko"},
Dog: &Dog{"inu"},
}
errs := map[string]error{}
errs["hasCat"] = validate.Struct(hasCat)
errs["hasBird"] = validate.Struct(hasBird)
errs["noPet"] = validate.Struct(noPet)
errs["hasTwo"] = validate.Struct(hasTwo)
for name, err := range errs {
fmt.Printf("%s: \n%+v\n\n", name, err)
}
}
出力結果は以下のようになります。
hasCat:
<nil>
hasBird:
<nil>
noPet:
Key: 'Pet.Cat' Error:Field validation for 'Cat' failed on the 'alternative' tag
Key: 'Pet.Dog' Error:Field validation for 'Dog' failed on the 'alternative' tag
Key: 'Pet.Bird' Error:Field validation for 'Bird' failed on the 'alternative' tag
hasTwo:
Key: 'Pet.Cat' Error:Field validation for 'Cat' failed on the 'alternative' tag
Key: 'Pet.Dog' Error:Field validation for 'Dog' failed on the 'alternative' tag
Key: 'Pet.Bird' Error:Field validation for 'Bird' failed on the 'alternative' tag
指定が1つもない場合や、複数指定された場合にエラーになります。ただし、お気づきの通り、エラーの場合はタグを指定したすべてのフィールドに対してエラーになります。どうにかする方法あるのかな…
バリエーション
今回は1つ指定必須でしたが、ちょっといじれば、
- 1つ以下しか指定出来ない
- 1つ以上指定
も簡単に作成できます。
余談
バリデーションの登録において、RegisterValidationの3つめの引数にtrueを指定しましたが、これはcallValidationEvenIfNull
というオプション引数で、フィールドがnilの場合でもバリデーションをするかどうかを指定するものになります。デフォルト値はfalseです。ここにtrueを指定しなかった場合の実行結果は以下のようになります。
hasCat:
Key: 'Pet.Dog' Error:Field validation for 'Dog' failed on the 'alternative' tag
Key: 'Pet.Bird' Error:Field validation for 'Bird' failed on the 'alternative' tag
hasBird:
Key: 'Pet.Cat' Error:Field validation for 'Cat' failed on the 'alternative' tag
Key: 'Pet.Dog' Error:Field validation for 'Dog' failed on the 'alternative' tag
noPet:
Key: 'Pet.Cat' Error:Field validation for 'Cat' failed on the 'alternative' tag
Key: 'Pet.Dog' Error:Field validation for 'Dog' failed on the 'alternative' tag
Key: 'Pet.Bird' Error:Field validation for 'Bird' failed on the 'alternative' tag
hasTwo:
Key: 'Pet.Cat' Error:Field validation for 'Cat' failed on the 'alternative' tag
Key: 'Pet.Dog' Error:Field validation for 'Dog' failed on the 'alternative' tag
Key: 'Pet.Bird' Error:Field validation for 'Bird' failed on the 'alternative' tag
フィールドを1つのみ指定しているhasCatやhasBirdもエラーになっていますね。
しかも何故か、hasCatではDogやBirdがエラーになったり、noPetでも全フィールドエラーになっています。どうやら、これがfalseだとnilの場合に単純にスキップされる訳ではなさそうです。
コードを見ると、validator.goの!ct.runValidationWhenNil
が真になりエラーになるようなのですが(⑥)、その前を見ると、typeがreflect.Ptr
で、ct.hasTag
がtrueのときにここに入っています(②, ⑤)。
また、その前に、extractTypeInternal
(①)において、ポインタかつnilでない場合は、実体を返却しているので、⑥に来る時点で、ポインタもしくはInterfaceだがnil、かつタグはあるということですね。
つまり、このオプションをtrueにしてタグを指定した場合は、その型がポインタもしくはInterfaceかつnil, もしくはInvalidである場合は、タグの内容に無関係にエラーになるという挙動のようです。
ただし、"omitempty","omitnil","isdefault"の方が強く、これらがある場合はエラーにはならないようです(③, ④)。
これは、
current, kind, v.fldIsPointer = v.extractTypeInternal(current, false) …①
var isNestedStruct bool
switch kind {
case reflect.Ptr, reflect.Interface, reflect.Invalid: …②
if ct == nil {
return
}
if ct.typeof == typeOmitEmpty || ct.typeof == typeIsDefault { …③
return
}
if ct.typeof == typeOmitNil && (kind != reflect.Invalid && current.IsNil()) { …④
return
}
if ct.hasTag { …⑤
if kind == reflect.Invalid {
v.str1 = string(append(ns, cf.altName...))
if v.v.hasTagNameFunc {
v.str2 = string(append(structNs, cf.name...))
} else {
v.str2 = v.str1
}
v.errs = append(v.errs,
&fieldError{
v: v.v,
tag: ct.aliasTag,
actualTag: ct.tag,
ns: v.str1,
structNs: v.str2,
fieldLen: uint8(len(cf.altName)),
structfieldLen: uint8(len(cf.name)),
param: ct.param,
kind: kind,
},
)
return
}
v.str1 = string(append(ns, cf.altName...))
if v.v.hasTagNameFunc {
v.str2 = string(append(structNs, cf.name...))
} else {
v.str2 = v.str1
}
if !ct.runValidationWhenNil { …⑥
v.errs = append(v.errs,
&fieldError{
v: v.v,
tag: ct.aliasTag,
actualTag: ct.tag,
ns: v.str1,
structNs: v.str2,
fieldLen: uint8(len(cf.altName)),
structfieldLen: uint8(len(cf.name)),
value: getValue(current),
param: ct.param,
kind: kind,
typ: current.Type(),
},
)
return
}
}
ちなみにデフォルトのvalidatorのtrue/falseはvalidator_instance.goに定義があります。
// must copy validators for separate validations to be used in each instance
for k, val := range bakedInValidators {
switch k {
// these require that even if the value is nil that the validation should run, omitempty still overrides this behaviour
case requiredIfTag, requiredUnlessTag, requiredWithTag, requiredWithAllTag, requiredWithoutTag, requiredWithoutAllTag,
excludedIfTag, excludedUnlessTag, excludedWithTag, excludedWithAllTag, excludedWithoutTag, excludedWithoutAllTag,
skipUnlessTag:
_ = v.registerValidation(k, wrapFunc(val), true, true)
default:
// no need to error check here, baked in will always be valid
_ = v.registerValidation(k, wrapFunc(val), true, false)
}
}
最後に
他にも作成したValidatorは載せていこうと思います!