validator

Go言語の構造体とフィールドバリデーションのための最も人気のあるライブラリ

validatorは、Go言語向けの最も人気のある構造体とフィールドバリデーションライブラリです。タグベースの宣言的なバリデーション、クロスフィールド検証、スライス・配列・マップのディープダイビング、カスタムバリデータ、i18n対応のエラーメッセージなど、包括的な機能を提供します。Ginフレームワークのデフォルトバリデータとしても採用されています。

主な特徴

  • タグベースのバリデーション: 構造体タグを使用した直感的なバリデーション定義
  • クロスフィールドバリデーション: 複数フィールド間の関係を検証
  • ディープダイビング: スライス、配列、マップの要素を再帰的に検証
  • カスタムバリデータ: 独自のバリデーションロジックを簡単に追加
  • 型安全: Go の型システムを活用した安全なバリデーション
  • 高速: ゼロアロケーションの最適化されたパフォーマンス
  • i18n対応: 多言語エラーメッセージのサポート
  • JSONフィールド名抽出: エラーメッセージにJSONタグ名を使用可能

インストール

# 最新版(v10)
go get github.com/go-playground/validator/v10

# モジュールを使用している場合
go mod download github.com/go-playground/validator/v10

基本的な使い方

パッケージのインポートと初期化

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

// バリデータインスタンスの作成
var validate *validator.Validate

func init() {
    validate = validator.New(validator.WithRequiredStructEnabled())
}

構造体のバリデーション

type User struct {
    FirstName      string     `validate:"required,alpha"`
    LastName       string     `validate:"required,alpha"`
    Age            uint8      `validate:"gte=0,lte=130"`
    Email          string     `validate:"required,email"`
    FavouriteColor string     `validate:"iscolor"`
    Addresses      []*Address `validate:"required,dive,required"`
}

type Address struct {
    Street string `validate:"required"`
    City   string `validate:"required"`
    Planet string `validate:"required"`
    Phone  string `validate:"required,e164"`
}

func main() {
    user := &User{
        FirstName:      "太郎",
        LastName:       "山田",
        Age:            25,
        Email:          "[email protected]",
        FavouriteColor: "#000-",
        Addresses: []*Address{
            {
                Street: "渋谷1-2-3",
                City:   "東京",
                Planet: "地球",
                Phone:  "+81312345678",
            },
        },
    }

    err := validate.Struct(user)
    if err != nil {
        // バリデーションエラーの処理
        for _, err := range err.(validator.ValidationErrors) {
            fmt.Println(err.Namespace())       // User.FirstName
            fmt.Println(err.Field())           // FirstName
            fmt.Println(err.StructNamespace()) // User.FirstName
            fmt.Println(err.StructField())     // FirstName
            fmt.Println(err.Tag())             // alpha
            fmt.Println(err.ActualTag())       // alpha
            fmt.Println(err.Kind())            // string
            fmt.Println(err.Type())            // string
            fmt.Println(err.Value())           // 太郎
            fmt.Println(err.Param())           // 
            fmt.Println()
        }
    }
}

組み込みバリデータ

基本的な型チェック

type BasicValidation struct {
    // 必須フィールド
    Required string `validate:"required"`
    
    // 数値の範囲
    Min      int `validate:"min=10"`
    Max      int `validate:"max=100"`
    Between  int `validate:"gte=10,lte=100"`
    
    // 文字列の長さ
    MinLen   string `validate:"min=5"`
    MaxLen   string `validate:"max=10"`
    Len      string `validate:"len=7"`
    
    // 等価性
    Equal    string `validate:"eq=test"`
    NotEqual string `validate:"ne=test"`
}

ネットワークとフォーマット

type NetworkValidation struct {
    // ネットワーク関連
    Email       string `validate:"email"`
    URL         string `validate:"url"`
    URI         string `validate:"uri"`
    IP          string `validate:"ip"`
    IPv4        string `validate:"ipv4"`
    IPv6        string `validate:"ipv6"`
    CIDR        string `validate:"cidr"`
    MAC         string `validate:"mac"`
    
    // ホスト名とドメイン
    Hostname    string `validate:"hostname"`
    FQDN        string `validate:"fqdn"`
    Port        string `validate:"hostname_port"`
    
    // データフォーマット
    UUID        string `validate:"uuid"`
    Base64      string `validate:"base64"`
    JSON        string `validate:"json"`
    JWT         string `validate:"jwt"`
    
    // 暗号通貨アドレス
    Bitcoin     string `validate:"btc_addr"`
    Ethereum    string `validate:"eth_addr"`
}

文字列バリデーション

type StringValidation struct {
    // 文字種別
    Alpha           string `validate:"alpha"`
    AlphaNum        string `validate:"alphanum"`
    Numeric         string `validate:"numeric"`
    Number          string `validate:"number"`
    Hexadecimal     string `validate:"hexadecimal"`
    HexColor        string `validate:"hexcolor"`
    
    // 大文字小文字
    Lowercase       string `validate:"lowercase"`
    Uppercase       string `validate:"uppercase"`
    
    // 含有チェック
    Contains        string `validate:"contains=test"`
    ContainsAny     string `validate:"containsany=!@#$"`
    ContainsRune    string `validate:"containsrune=☺"`
    Excludes        string `validate:"excludes=test"`
    ExcludesAll     string `validate:"excludesall=!@#$"`
    ExcludesRune    string `validate:"excludesrune=☺"`
    
    // 開始・終了
    StartsWith      string `validate:"startswith=Hello"`
    EndsWith        string `validate:"endswith=World"`
    StartsNotWith   string `validate:"startsnotwith=Bad"`
    EndsNotWith     string `validate:"endsnotwith=Bad"`
}

日付と時刻

type DateTimeValidation struct {
    // ISO8601形式
    DateTime    string    `validate:"datetime=2006-01-02"`
    
    // タイムゾーン
    Timezone    string    `validate:"timezone"`
    
    // 実際の日時型での比較も可能
    CreatedAt   time.Time `validate:"required"`
    UpdatedAt   time.Time `validate:"gtfield=CreatedAt"`
}

クロスフィールドバリデーション

フィールド間の比較

type CrossFieldExample struct {
    // パスワード確認
    Password        string `validate:"required,min=8"`
    PasswordConfirm string `validate:"required,eqfield=Password"`
    
    // 数値の比較
    MinPrice float64 `validate:"required"`
    MaxPrice float64 `validate:"required,gtfield=MinPrice"`
    
    // 日付の比較
    StartDate time.Time `validate:"required"`
    EndDate   time.Time `validate:"required,gtfield=StartDate"`
    
    // 条件付き必須
    ShipToOtherAddress bool   `validate:""`
    OtherAddress       string `validate:"required_if=ShipToOtherAddress true"`
}

複雑な条件付きバリデーション

type ConditionalValidation struct {
    // required_if: 特定フィールドが特定値の場合必須
    PaymentMethod string `validate:"required,oneof=card cash transfer"`
    CardNumber    string `validate:"required_if=PaymentMethod card"`
    
    // required_unless: 特定フィールドが特定値でない場合必須
    HasAccount bool   `validate:""`
    Email      string `validate:"required_unless=HasAccount true"`
    
    // required_with: 指定フィールドのいずれかが存在する場合必須
    FirstName  string `validate:""`
    LastName   string `validate:""`
    MiddleName string `validate:"required_with=FirstName LastName"`
    
    // excluded_if: 特定条件で除外
    IsAdmin    bool   `validate:""`
    AdminNotes string `validate:"excluded_if=IsAdmin false"`
}

カスタムバリデータ

シンプルなカスタムバリデータ

// 日本の郵便番号バリデータ
func ValidateJPZipCode(fl validator.FieldLevel) bool {
    zipCode := fl.Field().String()
    
    // 正規表現でフォーマットチェック
    matched, _ := regexp.MatchString(`^\d{3}-\d{4}$`, zipCode)
    return matched
}

// 登録
validate.RegisterValidation("jpzipcode", ValidateJPZipCode)

// 使用
type Address struct {
    ZipCode string `validate:"required,jpzipcode"`
}

パラメータ付きカスタムバリデータ

// 特定の値リストに含まれるかチェック
func ValidateValueIn(fl validator.FieldLevel) bool {
    // パラメータを取得(カンマ区切り)
    params := strings.Split(fl.Param(), " ")
    value := fl.Field().String()
    
    for _, param := range params {
        if value == param {
            return true
        }
    }
    return false
}

validate.RegisterValidation("valuein", ValidateValueIn)

// 使用例
type Status struct {
    State string `validate:"valuein=active inactive pending"`
}

構造体レベルのバリデーション

// 構造体全体のバリデーション
func UserStructLevelValidation(sl validator.StructLevel) {
    user := sl.Current().Interface().(User)

    // ビジネスロジックに基づく検証
    if user.Age < 18 && user.ParentConsent == false {
        sl.ReportError(user.ParentConsent, "ParentConsent", "ParentConsent", "parentconsent", "")
    }

    // 複数フィールドの組み合わせ検証
    if user.Country == "JP" && !strings.HasPrefix(user.Phone, "+81") {
        sl.ReportError(user.Phone, "Phone", "Phone", "jpphone", "")
    }
}

// 登録
validate.RegisterStructValidation(UserStructLevelValidation, User{})

エラーハンドリング

詳細なエラー情報の取得

err := validate.Struct(user)
if err != nil {
    // すべてのエラーを処理
    if validationErrors, ok := err.(validator.ValidationErrors); ok {
        for _, fieldError := range validationErrors {
            // エラー情報を日本語で構築
            switch fieldError.Tag() {
            case "required":
                fmt.Printf("%sは必須です\n", fieldError.Field())
            case "email":
                fmt.Printf("%sは有効なメールアドレスではありません\n", fieldError.Field())
            case "min":
                fmt.Printf("%sは%s文字以上である必要があります\n", 
                    fieldError.Field(), fieldError.Param())
            }
        }
    }
}

JSONフィールド名でのエラー表示

// JSONタグ名を使用する設定
validate.RegisterTagNameFunc(func(fld reflect.StructField) string {
    name := strings.SplitN(fld.Tag.Get("json"), ",", 2)[0]
    if name == "-" {
        return ""
    }
    return name
})

type User struct {
    FirstName string `json:"first_name" validate:"required"`
    Email     string `json:"email" validate:"required,email"`
}

// エラーメッセージでJSONフィールド名が使用される
// 例: "first_name is required"

高度な使い方

スライスとマップのディープダイビング

type Order struct {
    // スライスの各要素を検証
    Items []OrderItem `validate:"required,dive,required"`
    
    // マップのキーと値を検証
    Metadata map[string]string `validate:"required,dive,keys,alphanum,endkeys,required,max=255"`
    
    // ネストしたスライスの検証
    Categories [][]string `validate:"required,dive,dive,required"`
}

type OrderItem struct {
    ProductID string  `validate:"required,uuid"`
    Quantity  int     `validate:"required,min=1"`
    Price     float64 `validate:"required,gt=0"`
}

エイリアスの定義

// 複数のバリデーションを1つのタグにまとめる
validate.RegisterAlias("strongpassword", "required,min=8,max=128,containsany=!@#$%^&*")

type Account struct {
    Password string `validate:"strongpassword"`
}

翻訳とi18n

import (
    "github.com/go-playground/validator/v10"
    ja_translations "github.com/go-playground/validator/v10/translations/ja"
    "github.com/go-playground/locales/ja"
    ut "github.com/go-playground/universal-translator"
)

// 日本語翻訳の設定
ja := ja.New()
uni := ut.New(ja, ja)
trans, _ := uni.GetTranslator("ja")

// 翻訳の登録
ja_translations.RegisterDefaultTranslations(validate, trans)

// カスタム翻訳の追加
validate.RegisterTranslation("jpzipcode", trans, func(ut ut.Translator) error {
    return ut.Add("jpzipcode", "{0}は正しい郵便番号形式(123-4567)である必要があります", true)
}, func(ut ut.Translator, fe validator.FieldError) string {
    t, _ := ut.T("jpzipcode", fe.Field())
    return t
})

パフォーマンス最適化

キャッシングの活用

// バリデータの再利用
var (
    validate = validator.New()
    userValidator = validate.Struct
)

// 構造体のキャッシング
type cachedValidator struct {
    validate *validator.Validate
    cache    sync.Map
}

func (cv *cachedValidator) ValidateStruct(s interface{}) error {
    key := reflect.TypeOf(s)
    if cached, ok := cv.cache.Load(key); ok {
        return cached.(func(interface{}) error)(s)
    }
    
    // キャッシュミス時は通常のバリデーション
    return cv.validate.Struct(s)
}

ベストプラクティス

  1. 明確なタグ名: わかりやすいカスタムタグ名を使用
  2. エラーメッセージの国際化: 多言語対応を考慮
  3. バリデーションの分離: ビジネスロジックと分離
  4. パフォーマンス: 頻繁に使用する構造体はキャッシュ
  5. テスト: カスタムバリデータには必ずテストを作成

Ginフレームワークとの連携

import (
    "github.com/gin-gonic/gin"
    "github.com/gin-gonic/gin/binding"
)

// Ginのデフォルトバリデータを置き換え
func setupValidator() {
    if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
        // カスタムバリデータの登録
        v.RegisterValidation("jpzipcode", ValidateJPZipCode)
        
        // JSONタグ名の使用
        v.RegisterTagNameFunc(func(fld reflect.StructField) string {
            name := strings.SplitN(fld.Tag.Get("json"), ",", 2)[0]
            if name == "-" {
                return ""
            }
            return name
        })
    }
}

// コントローラーでの使用
func CreateUser(c *gin.Context) {
    var user User
    if err := c.ShouldBindJSON(&user); err != nil {
        c.JSON(400, gin.H{"error": err.Error()})
        return
    }
    
    // バリデーション成功
    c.JSON(200, gin.H{"message": "User created"})
}