ozzo-validation

Go言語のための構成可能で高速なルールベースのデータバリデーションライブラリ

ozzo-validationは、Go言語向けの構成可能でカスタマイズ可能なデータバリデーションライブラリです。ルールベースのアプローチを採用し、単純な値から複雑な構造体まで、あらゆるデータ型のバリデーションを簡潔かつ直感的に行えます。

主な特徴

  • ルールベースのバリデーション: 再利用可能なバリデーションルールの組み合わせ
  • 構成可能: ルールを組み合わせて複雑なバリデーションロジックを構築
  • エラーメッセージのカスタマイズ: 多言語対応を含む柔軟なエラーメッセージ設定
  • 型に依存しない: interface{} を使用して任意の型をバリデート
  • コンテキスト対応: 他のフィールドの値に基づく条件付きバリデーション
  • 高速: リフレクションの使用を最小限に抑えた効率的な実装
  • 拡張可能: カスタムルールの作成が簡単

インストール

# 最新版(v4)
go get github.com/go-ozzo/ozzo-validation/v4

# is パッケージ(一般的なバリデーションルール)も推奨
go get github.com/go-ozzo/ozzo-validation/v4/is

基本的な使い方

パッケージのインポート

import (
    validation "github.com/go-ozzo/ozzo-validation/v4"
    "github.com/go-ozzo/ozzo-validation/v4/is"
)

単純な値のバリデーション

// 文字列のバリデーション
email := "[email protected]"
err := validation.Validate(email,
    validation.Required,       // 必須
    is.Email,                 // メールアドレス形式
)

// 数値のバリデーション
age := 25
err := validation.Validate(age,
    validation.Required,
    validation.Min(18),       // 最小値
    validation.Max(100),      // 最大値
)

// スライスのバリデーション
tags := []string{"go", "validation", ""}
err := validation.Validate(tags,
    validation.Required,
    validation.Length(1, 5),  // 要素数の範囲
    validation.Each(          // 各要素に対するルール
        validation.Required,
        validation.Length(1, 20),
    ),
)

構造体のバリデーション

基本的な構造体バリデーション

type Address struct {
    Street  string
    City    string
    State   string
    Zip     string
}

func (a Address) Validate() error {
    return validation.ValidateStruct(&a,
        // Street フィールドのルール
        validation.Field(&a.Street, validation.Required, validation.Length(1, 100)),
        // City フィールドのルール
        validation.Field(&a.City, validation.Required, validation.Length(1, 50)),
        // State フィールドのルール
        validation.Field(&a.State, validation.Required, validation.Length(2, 2)),
        // Zip フィールドのルール(日本の郵便番号形式)
        validation.Field(&a.Zip, validation.Required, validation.Match(regexp.MustCompile("^\\d{3}-\\d{4}$"))),
    )
}

ネストした構造体のバリデーション

type User struct {
    Name     string
    Email    string
    Password string
    Profile  Profile
    Address  Address
}

type Profile struct {
    Age     int
    Phone   string
    Website string
}

func (p Profile) Validate() error {
    return validation.ValidateStruct(&p,
        validation.Field(&p.Age, validation.Required, validation.Min(0), validation.Max(120)),
        validation.Field(&p.Phone, validation.Match(regexp.MustCompile("^0\\d{1,4}-\\d{1,4}-\\d{4}$"))),
        validation.Field(&p.Website, is.URL),
    )
}

func (u User) Validate() error {
    return validation.ValidateStruct(&u,
        validation.Field(&u.Name, validation.Required, validation.Length(2, 50)),
        validation.Field(&u.Email, validation.Required, is.Email),
        validation.Field(&u.Password, validation.Required, validation.Length(8, 100)),
        validation.Field(&u.Profile), // Profile.Validate() が自動的に呼ばれる
        validation.Field(&u.Address), // Address.Validate() が自動的に呼ばれる
    )
}

組み込みバリデーションルール

一般的なルール

// 必須チェック
validation.Required

// nil 禁止
validation.NotNil

// 長さ・サイズ
validation.Length(min, max)  // 文字列、スライス、マップ、配列の長さ
validation.RuneLength(min, max) // 文字列の文字数(ルーン数)

// 数値の範囲
validation.Min(minValue)
validation.Max(maxValue)

// 含まれる/含まれない
validation.In("apple", "orange", "banana")
validation.NotIn("admin", "root", "superuser")

// 正規表現マッチ
validation.Match(regexp.MustCompile("^[A-Z]+$"))

// 日付
validation.Date("2006-01-02") // 指定フォーマットの日付文字列

is パッケージのルール

// メールアドレス
is.Email

// URL
is.URL
is.RequestURL
is.RequestURI

// アルファベット・数字
is.Alpha        // アルファベットのみ
is.Digit        // 数字のみ
is.Alphanumeric // アルファベットと数字
is.LowerCase    // 小文字のみ
is.UpperCase    // 大文字のみ

// ネットワーク
is.IP           // IPアドレス(v4 または v6)
is.IPv4         // IPv4アドレス
is.IPv6         // IPv6アドレス
is.MAC          // MACアドレス
is.CIDR         // CIDR記法
is.Host         // ホスト名
is.Port         // ポート番号
is.DNSName      // DNS名

// 識別子
is.UUID         // UUID
is.UUIDv3       // UUID version 3
is.UUIDv4       // UUID version 4
is.UUIDv5       // UUID version 5

// その他
is.JSON         // 有効なJSON
is.Base64       // Base64エンコード
is.ASCII        // ASCII文字のみ
is.PrintableASCII // 印刷可能なASCII文字
is.Multibyte    // マルチバイト文字を含む
is.FullWidth    // 全角文字を含む
is.HalfWidth    // 半角文字を含む
is.CreditCard   // クレジットカード番号(Luhnアルゴリズム)

カスタムバリデーションルール

シンプルなカスタムルール

// 日本の電話番号バリデーションルール
var JapanesePhoneNumber = validation.NewStringRule(
    func(value string) bool {
        // 固定電話または携帯電話の形式
        pattern := `^(0[0-9]{1,4}-[0-9]{1,4}-[0-9]{4}|0[789]0-[0-9]{4}-[0-9]{4})$`
        matched, _ := regexp.MatchString(pattern, value)
        return matched
    },
    "は有効な日本の電話番号ではありません",
)

// 使用例
phone := "090-1234-5678"
err := validation.Validate(phone, validation.Required, JapanesePhoneNumber)

Rule インターフェースを実装

// パスワード強度チェックルール
type PasswordStrengthRule struct {
    MinLength      int
    RequireUpper   bool
    RequireLower   bool
    RequireNumber  bool
    RequireSpecial bool
}

func (r PasswordStrengthRule) Validate(value interface{}) error {
    s, ok := value.(string)
    if !ok {
        return fmt.Errorf("パスワードは文字列である必要があります")
    }
    
    if len(s) < r.MinLength {
        return fmt.Errorf("パスワードは%d文字以上である必要があります", r.MinLength)
    }
    
    if r.RequireUpper && !strings.ContainsAny(s, "ABCDEFGHIJKLMNOPQRSTUVWXYZ") {
        return fmt.Errorf("パスワードには大文字を含める必要があります")
    }
    
    if r.RequireLower && !strings.ContainsAny(s, "abcdefghijklmnopqrstuvwxyz") {
        return fmt.Errorf("パスワードには小文字を含める必要があります")
    }
    
    if r.RequireNumber && !strings.ContainsAny(s, "0123456789") {
        return fmt.Errorf("パスワードには数字を含める必要があります")
    }
    
    if r.RequireSpecial && !strings.ContainsAny(s, "!@#$%^&*()_+-=[]{}|;:,.<>?") {
        return fmt.Errorf("パスワードには特殊文字を含める必要があります")
    }
    
    return nil
}

// 使用例
password := "MyP@ssw0rd"
err := validation.Validate(password, PasswordStrengthRule{
    MinLength:      8,
    RequireUpper:   true,
    RequireLower:   true,
    RequireNumber:  true,
    RequireSpecial: true,
})

条件付きバリデーション

When を使用した条件付きルール

type Order struct {
    PaymentMethod string
    CardNumber    string
    CardCVV       string
    BankAccount   string
}

func (o Order) Validate() error {
    return validation.ValidateStruct(&o,
        validation.Field(&o.PaymentMethod, 
            validation.Required, 
            validation.In("credit_card", "bank_transfer", "cash"),
        ),
        // クレジットカードの場合のみカード情報を必須に
        validation.Field(&o.CardNumber,
            validation.When(o.PaymentMethod == "credit_card",
                validation.Required,
                is.CreditCard,
            ),
        ),
        validation.Field(&o.CardCVV,
            validation.When(o.PaymentMethod == "credit_card",
                validation.Required,
                validation.Match(regexp.MustCompile("^\\d{3,4}$")),
            ),
        ),
        // 銀行振込の場合のみ口座番号を必須に
        validation.Field(&o.BankAccount,
            validation.When(o.PaymentMethod == "bank_transfer",
                validation.Required,
                validation.Match(regexp.MustCompile("^\\d{7}$")),
            ),
        ),
    )
}

By を使用した動的ルール

type PasswordChangeRequest struct {
    CurrentPassword string
    NewPassword     string
    ConfirmPassword string
}

func (r PasswordChangeRequest) Validate() error {
    return validation.ValidateStruct(&r,
        validation.Field(&r.CurrentPassword, validation.Required),
        validation.Field(&r.NewPassword, 
            validation.Required,
            validation.Length(8, 100),
            // 現在のパスワードと異なることを確認
            validation.By(func(value interface{}) error {
                if r.CurrentPassword == value.(string) {
                    return errors.New("新しいパスワードは現在のパスワードと異なる必要があります")
                }
                return nil
            }),
        ),
        validation.Field(&r.ConfirmPassword,
            validation.Required,
            // 新しいパスワードと一致することを確認
            validation.By(func(value interface{}) error {
                if r.NewPassword != value.(string) {
                    return errors.New("確認用パスワードが一致しません")
                }
                return nil
            }),
        ),
    )
}

エラーハンドリング

エラー構造の取得

user := User{
    Name:  "",
    Email: "invalid-email",
    Profile: Profile{
        Age: -1,
    },
}

err := user.Validate()
if err != nil {
    // validation.Errors 型にキャスト
    if e, ok := err.(validation.Errors); ok {
        // フィールドごとのエラーを取得
        for field, fieldErr := range e {
            fmt.Printf("フィールド %s: %v\n", field, fieldErr)
        }
    }
}

カスタムエラーメッセージ

// ルールごとのエラーメッセージカスタマイズ
err := validation.Validate(email,
    validation.Required.Error("メールアドレスは必須です"),
    is.Email.Error("有効なメールアドレスを入力してください"),
)

// グローバルなエラーメッセージの設定
validation.ErrorTag = "ja"  // エラーメッセージの言語タグ

// カスタムエラーメッセージテンプレート
type CustomRequiredRule struct{}

func (r CustomRequiredRule) Validate(value interface{}) error {
    return validation.Validate(value, validation.Required)
}

func (r CustomRequiredRule) Error(message string) validation.Rule {
    return validation.Required.Error(message)
}

実用的な例

RESTful API のリクエストバリデーション

// ユーザー登録リクエスト
type RegisterRequest struct {
    Username        string `json:"username"`
    Email          string `json:"email"`
    Password       string `json:"password"`
    PasswordConfirm string `json:"password_confirm"`
    Age            int    `json:"age"`
    AcceptTerms    bool   `json:"accept_terms"`
}

func (r RegisterRequest) Validate() error {
    return validation.ValidateStruct(&r,
        // ユーザー名: 3-20文字、英数字とアンダースコアのみ
        validation.Field(&r.Username,
            validation.Required,
            validation.Length(3, 20),
            validation.Match(regexp.MustCompile("^[a-zA-Z0-9_]+$")).
                Error("ユーザー名は英数字とアンダースコアのみ使用できます"),
        ),
        // メールアドレス
        validation.Field(&r.Email,
            validation.Required,
            is.Email,
            // 重複チェック(カスタムルール)
            validation.By(checkEmailUniqueness),
        ),
        // パスワード
        validation.Field(&r.Password,
            validation.Required,
            validation.Length(8, 100),
            PasswordStrengthRule{
                MinLength:      8,
                RequireUpper:   true,
                RequireLower:   true,
                RequireNumber:  true,
                RequireSpecial: false,
            },
        ),
        // パスワード確認
        validation.Field(&r.PasswordConfirm,
            validation.Required,
            validation.By(func(value interface{}) error {
                if r.Password != value.(string) {
                    return errors.New("パスワードが一致しません")
                }
                return nil
            }),
        ),
        // 年齢
        validation.Field(&r.Age,
            validation.Required,
            validation.Min(13).Error("13歳以上である必要があります"),
            validation.Max(120),
        ),
        // 利用規約への同意
        validation.Field(&r.AcceptTerms,
            validation.By(func(value interface{}) error {
                if !value.(bool) {
                    return errors.New("利用規約に同意する必要があります")
                }
                return nil
            }),
        ),
    )
}

func checkEmailUniqueness(value interface{}) error {
    email := value.(string)
    // データベースでメールアドレスの重複をチェック
    exists, err := db.EmailExists(email)
    if err != nil {
        return err
    }
    if exists {
        return errors.New("このメールアドレスは既に使用されています")
    }
    return nil
}

設定ファイルのバリデーション

type ServerConfig struct {
    Host         string            `json:"host"`
    Port         int               `json:"port"`
    TLS          TLSConfig         `json:"tls"`
    Database     DatabaseConfig    `json:"database"`
    Cache        CacheConfig       `json:"cache"`
    RateLimiting RateLimitConfig   `json:"rate_limiting"`
}

type TLSConfig struct {
    Enabled  bool   `json:"enabled"`
    CertFile string `json:"cert_file"`
    KeyFile  string `json:"key_file"`
}

type DatabaseConfig struct {
    Driver   string `json:"driver"`
    Host     string `json:"host"`
    Port     int    `json:"port"`
    Username string `json:"username"`
    Password string `json:"password"`
    Database string `json:"database"`
    MaxConns int    `json:"max_conns"`
}

func (c ServerConfig) Validate() error {
    return validation.ValidateStruct(&c,
        validation.Field(&c.Host, validation.Required, is.Host),
        validation.Field(&c.Port, validation.Required, is.Port),
        validation.Field(&c.TLS),
        validation.Field(&c.Database),
        validation.Field(&c.Cache),
        validation.Field(&c.RateLimiting),
    )
}

func (t TLSConfig) Validate() error {
    return validation.ValidateStruct(&t,
        validation.Field(&t.CertFile,
            validation.When(t.Enabled, validation.Required),
        ),
        validation.Field(&t.KeyFile,
            validation.When(t.Enabled, validation.Required),
        ),
    )
}

func (d DatabaseConfig) Validate() error {
    return validation.ValidateStruct(&d,
        validation.Field(&d.Driver, validation.Required, validation.In("mysql", "postgres", "sqlite")),
        validation.Field(&d.Host, validation.When(d.Driver != "sqlite", validation.Required, is.Host)),
        validation.Field(&d.Port, validation.When(d.Driver != "sqlite", validation.Required, is.Port)),
        validation.Field(&d.Username, validation.When(d.Driver != "sqlite", validation.Required)),
        validation.Field(&d.Password, validation.When(d.Driver != "sqlite", validation.Required)),
        validation.Field(&d.Database, validation.Required),
        validation.Field(&d.MaxConns, validation.Min(1), validation.Max(100)),
    )
}

パフォーマンスとベストプラクティス

パフォーマンスの最適化

// ルールの再利用
var (
    emailRule = []validation.Rule{
        validation.Required,
        is.Email,
    }
    
    passwordRule = []validation.Rule{
        validation.Required,
        validation.Length(8, 100),
    }
)

// 正規表現のプリコンパイル
var (
    phoneRegex = regexp.MustCompile(`^0\d{1,4}-\d{1,4}-\d{4}$`)
    zipRegex   = regexp.MustCompile(`^\d{3}-\d{4}$`)
)

ベストプラクティス

  1. Validate メソッドの実装: 構造体には Validate() error メソッドを実装
  2. エラーメッセージの一貫性: アプリケーション全体で統一されたエラーメッセージ
  3. 再利用可能なルール: 共通のバリデーションルールは変数として定義
  4. テスタビリティ: バリデーションロジックは単体テストを作成
  5. 段階的なバリデーション: 基本的なチェックから複雑なチェックへ
// テストの例
func TestUserValidation(t *testing.T) {
    tests := []struct {
        name    string
        user    User
        wantErr bool
    }{
        {
            name: "有効なユーザー",
            user: User{
                Name:     "田中太郎",
                Email:    "[email protected]",
                Password: "MyP@ssw0rd",
            },
            wantErr: false,
        },
        {
            name: "無効なメールアドレス",
            user: User{
                Name:     "田中太郎",
                Email:    "invalid-email",
                Password: "MyP@ssw0rd",
            },
            wantErr: true,
        },
    }
    
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            err := tt.user.Validate()
            if (err != nil) != tt.wantErr {
                t.Errorf("Validate() error = %v, wantErr %v", err, tt.wantErr)
            }
        })
    }
}

まとめ

ozzo-validationは、Go言語で柔軟かつ強力なバリデーションを実現するための優れたライブラリです。ルールベースのアプローチにより、シンプルなバリデーションから複雑なビジネスルールまで、クリーンで保守しやすいコードで実装できます。カスタムルールの作成も容易で、アプリケーションの要求に応じて拡張できる点が大きな利点です。