Go の Gin でのバリデーション

Gin は go-playground/validator を内蔵しており、構造体タグでバリデーションルールを定義できる。入力検証を宣言的に行えるため、コードがすっきりする。

基本的なバリデーション

構造体フィールドに binding タグを付けるとバリデーションが有効になる。

type CreateUserRequest struct {
    Name  string `json:"name" binding:"required"`
    Email string `json:"email" binding:"required,email"`
    Age   int    `json:"age" binding:"required,gte=0,lte=120"`
}

r.POST("/users", func(c *gin.Context) {
    var req CreateUserRequest
    
    if err := c.ShouldBindJSON(&req); err != nil {
        c.JSON(400, gin.H{"error": err.Error()})
        return
    }
    
    // バリデーション通過
    c.JSON(201, gin.H{"user": req})
})

バリデーションに失敗すると ShouldBindJSON がエラーを返す。

よく使うバリデーションタグ

required

値が必須。ゼロ値(空文字、0、nil など)は許可されない。

email

メールアドレス形式かどうかをチェックする。

gte, lte, gt, lt

数値の範囲を指定。gte=0 は「0以上」、lte=100 は「100以下」。

min, max

文字列の長さや配列の要素数の範囲。min=3 は「3文字以上」。

len

文字列の長さや配列の要素数が正確にその値かどうか。

oneof

指定した値のいずれかであること。oneof=male female other のように使う。

複数のバリデーションルール

カンマで区切って複数のルールを指定できる。

type Product struct {
    Name     string  `json:"name" binding:"required,min=2,max=100"`
    Price    float64 `json:"price" binding:"required,gt=0"`
    Category string  `json:"category" binding:"required,oneof=electronics books clothing"`
    SKU      string  `json:"sku" binding:"required,len=10"`
}

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

ネストした構造体も自動的にバリデーションされる。

type Address struct {
    Street  string `json:"street" binding:"required"`
    City    string `json:"city" binding:"required"`
    ZipCode string `json:"zip_code" binding:"required,len=7"`
}

type Order struct {
    ProductID int     `json:"product_id" binding:"required,gt=0"`
    Quantity  int     `json:"quantity" binding:"required,gte=1,lte=100"`
    Shipping  Address `json:"shipping" binding:"required"`
}

dive タグを使うとスライス内の各要素もバリデーションできる。

type BulkOrder struct {
    Items []OrderItem `json:"items" binding:"required,dive"`
}

type OrderItem struct {
    ProductID int `json:"product_id" binding:"required,gt=0"`
    Quantity  int `json:"quantity" binding:"required,gte=1"`
}

カスタムバリデーションの作成

独自のバリデーションルールを定義できる。

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

// カスタムバリデーション関数
func validateUsername(fl validator.FieldLevel) bool {
    username := fl.Field().String()
    // 英数字とアンダースコアのみ許可
    matched, _ := regexp.MatchString(`^[a-zA-Z0-9_]+$`, username)
    return matched
}

func main() {
    r := gin.Default()
    
    // バリデーターを取得して登録
    if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
        v.RegisterValidation("username", validateUsername)
    }
    
    // 使用
    type User struct {
        Username string `json:"username" binding:"required,username"`
    }
}

エラーメッセージのカスタマイズ

デフォルトのエラーメッセージは技術的で、ユーザーに見せるには不親切だ。

func formatValidationErrors(err error) map[string]string {
    errors := make(map[string]string)
    
    if validationErrors, ok := err.(validator.ValidationErrors); ok {
        for _, e := range validationErrors {
            field := e.Field()
            switch e.Tag() {
            case "required":
                errors[field] = field + " は必須です"
            case "email":
                errors[field] = "有効なメールアドレスを入力してください"
            case "min":
                errors[field] = field + " は " + e.Param() + " 文字以上必要です"
            case "max":
                errors[field] = field + " は " + e.Param() + " 文字以下にしてください"
            default:
                errors[field] = field + " が無効です"
            }
        }
    }
    
    return errors
}

r.POST("/users", func(c *gin.Context) {
    var req CreateUserRequest
    
    if err := c.ShouldBindJSON(&req); err != nil {
        c.JSON(400, gin.H{
            "errors": formatValidationErrors(err),
        })
        return
    }
    // ...
})

条件付きバリデーション

required_if などのタグを使って、他のフィールドの値に応じたバリデーションも可能だ。

type Payment struct {
    Method     string `json:"method" binding:"required,oneof=card bank"`
    CardNumber string `json:"card_number" binding:"required_if=Method card"`
    BankCode   string `json:"bank_code" binding:"required_if=Method bank"`
}

Method が “card” の場合は CardNumber が必須、“bank” の場合は BankCode が必須になる。

omitempty との組み合わせ

更新 API など、一部のフィールドだけ送信される場合は omitempty を使う。

type UpdateUserRequest struct {
    Name  string `json:"name" binding:"omitempty,min=2,max=100"`
    Email string `json:"email" binding:"omitempty,email"`
    Age   int    `json:"age" binding:"omitempty,gte=0,lte=120"`
}

omitempty を付けると、値がゼロ値の場合はバリデーションをスキップする。値が存在する場合のみ、その他のルールが適用される。