go の構造体の cross field validation

はじめに

goccy/go-yamlgo-playground/validator を使って構造体のフィールド間の validation をする方法をまとめます。

github.com

go の 構造体の cross field validation

go-playground/validatorのドキュメント にある通り構造体の tag としての cross field validation は gtfieldltfield など単純な値の比較のみが提供されているようです。

今回は Custom Validation Functions を使って構造体のフィールド間の validation を行います。

Custom Validation Functions

RegisterStructValidation() を使って構造体ごとまるっと引っ張ってきて validation を行います。

今回は以下のような構造体を扱います。

package main

import (
    "fmt"
    "strings"

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

type API struct {
    A int
    B string
}

var yml = `---
a: 10
b: "small"
`

func main() {
    var api API

    validate := validator.New()
    validate.RegisterStructValidation(custom_validation, api)

    dec := yaml.NewDecoder(
        strings.NewReader(yml),
        // yaml.Validator(validate), // <- ここで呼べないので
    )

    err := dec.Decode(&api)
    if err != nil {
        fmt.Println(yaml.FormatError(err, true, true))
    }

    // ここで呼ぶ
    err = validate.Struct(api)
    if err != nil {
        fmt.Println(err)
    }

}

func custom_validation(sl validator.StructLevel) {
    api := sl.Current().Interface().(API)
    if api.A > 5 && api.B == "small" {
        source, err := yamlSourceByPath(yml, "$.b")
        if err != nil {
            panic(err)
        }
        fmt.Printf("b value expected \"large\" but actual %s:\n%s\n", api.B, source)
    }
}

// https://github.com/goccy/go-yaml#51-print-customized-error-with-yaml-source-code
func yamlSourceByPath(originalSource string, pathStr string) (string, error) {
    file, err := parser.ParseBytes([]byte(originalSource), 0)
    if err != nil {
        return "", err
    }
    path, err := yaml.PathString(pathStr)
    if err != nil {
        return "", err
    }
    node, err := path.FilterFile(file)
    if err != nil {
        return "", err
    }
    var p printer.Printer
    return p.PrintErrorToken(node.GetToken(), true), nil
}

yaml.NewDecoder() は validator を渡すことで invalid な箇所がとても綺麗に出力される魅力があるのですが RegisterStructValidation() を利用した時に invalid すると SIGSEGV するので validate.Struct(api) のように Decoder の外で呼ぶことにしました。

追記
goccy/go-yaml の author様に YAMLPath なるものを教えていただき Decoder 外の validator でも綺麗に出力することができました。

$ go run main.go 
b value expected "large" but actual small:
   1 | ---
   2 | a: 10
>  3 | b: "small"
          ^

まとめ

go-playground/validator を使って構造体のフィールド間の validation をする方法をまとめました。

github.com

参考サイト
qiita.com