Struct Tags

Validate structs using go-playground/validator tags

Use struct tags with go-playground/validator syntax to validate your structs. This is the most common validation strategy in the Rivaas validation package.

Basic Syntax

Add validate tags to struct fields:

type User struct {
    Email    string `validate:"required,email"`
    Age      int    `validate:"min=18,max=120"`
    Username string `validate:"required,min=3,max=20"`
}

Tags are comma-separated constraints. Each constraint is evaluated, and all must pass for validation to succeed.

Common Validation Tags

Required Fields

type User struct {
    Email string `validate:"required"`      // Must be non-zero value
    Name  string `validate:"required"`      // Must be non-empty string
    Age   int    `validate:"required"`      // Must be non-zero number
}

String Constraints

type User struct {
    // Length constraints
    Username string `validate:"min=3,max=20"`
    Bio      string `validate:"max=500"`
    
    // Format constraints
    Email    string `validate:"email"`
    URL      string `validate:"url"`
    UUID     string `validate:"uuid"`
    
    // Character constraints
    AlphaOnly string `validate:"alpha"`
    AlphaNum  string `validate:"alphanum"`
    Numeric   string `validate:"numeric"`
}

Number Constraints

type Product struct {
    Price    float64 `validate:"min=0"`
    Quantity int     `validate:"min=1,max=1000"`
    Rating   float64 `validate:"gte=0,lte=5"`  // Greater/less than or equal
}

Comparison Operators

TagDescription
min=NMinimum value (numbers) or length (strings/slices)
max=NMaximum value (numbers) or length (strings/slices)
eq=NEqual to N
ne=NNot equal to N
gt=NGreater than N
gte=NGreater than or equal to N
lt=NLess than N
lte=NLess than or equal to N

Enum Values

type Order struct {
    Status string `validate:"oneof=pending confirmed shipped delivered"`
}

Multiple values separated by spaces.

Collection Constraints

type Request struct {
    Tags   []string `validate:"min=1,max=10,dive,min=2,max=20"`
    //                         ^^^^^^^^^^^^  ^^^^^^^^^^^^^^^^
    //                         Array rules   Element rules
    
    Emails []string `validate:"required,dive,email"`
}

Use dive to validate elements inside slices/arrays/maps.

Nested Structs

type Address struct {
    Street string `validate:"required"`
    City   string `validate:"required"`
    Zip    string `validate:"required,numeric,len=5"`
}

type User struct {
    Name    string  `validate:"required"`
    Address Address `validate:"required"` // Validates nested struct
}

Pointer Fields

type User struct {
    Email *string `validate:"omitempty,email"`
    //                      ^^^^^^^^^ Skip validation if nil
}

Use omitempty to skip validation when the field is nil or zero-value.

Format Validation Tags

Email and Web

type Contact struct {
    Email     string `validate:"email"`
    Website   string `validate:"url"`
    Hostname  string `validate:"hostname"`
    IPAddress string `validate:"ip"`
    IPv4      string `validate:"ipv4"`
    IPv6      string `validate:"ipv6"`
}

File Paths

type Config struct {
    DataFile string `validate:"file"`      // Must be existing file
    DataDir  string `validate:"dir"`       // Must be existing directory
    FilePath string `validate:"filepath"`  // Valid file path syntax
}

Identifiers

type Resource struct {
    ID       string `validate:"uuid"`
    UUID4    string `validate:"uuid4"`
    ISBN     string `validate:"isbn"`
    CreditCard string `validate:"credit_card"`
}

Cross-Field Validation

Field Comparison

type Registration struct {
    Password        string `validate:"required,min=8"`
    ConfirmPassword string `validate:"required,eqfield=Password"`
    //                                         ^^^^^^^^^^^^^^^^
    //                                         Must equal Password field
}

Conditional Validation

type User struct {
    Type  string `validate:"oneof=personal business"`
    TaxID string `validate:"required_if=Type business"`
    //                      ^^^^^^^^^^^^^^^^^^^^^^^^
    //                      Required when Type is "business"
}

Cross-field tags:

TagDescription
eqfield=FieldMust equal another field
nefield=FieldMust not equal another field
gtfield=FieldMust be greater than another field
ltfield=FieldMust be less than another field
required_if=Field ValueRequired when field equals value
required_unless=Field ValueRequired unless field equals value
required_with=FieldRequired when field is present
required_without=FieldRequired when field is absent

Advanced Tags

Regular Expressions

type User struct {
    Phone string `validate:"required,e164"`           // E.164 phone format
    Slug  string `validate:"required,alphanum,min=3"` // URL-safe slug
}

Boolean Logic

type Product struct {
    // Must be numeric OR alpha
    Code string `validate:"numeric|alpha"`
}

Use | (OR) to allow multiple constraint sets.

Custom Formats

type Data struct {
    Datetime string `validate:"datetime=2006-01-02"`
    Date     string `validate:"datetime=2006-01-02 15:04:05"`
}

Tag Naming with JSON

By default, validation uses JSON field names in error messages:

type User struct {
    Email string `json:"email_address" validate:"required,email"`
    //            ^^^^^^^^^^^^^^^^^^^ Used in error message
}

Error message will reference email_address, not Email.

Validation Example

Complete example with various constraints:

package main

import (
    "context"
    "fmt"
    "rivaas.dev/validation"
)

type CreateUserRequest struct {
    // Required string with length constraints
    Username string `json:"username" validate:"required,min=3,max=20,alphanum"`
    
    // Valid email address
    Email string `json:"email" validate:"required,email"`
    
    // Age range
    Age int `json:"age" validate:"required,min=18,max=120"`
    
    // Password with confirmation
    Password        string `json:"password" validate:"required,min=8"`
    ConfirmPassword string `json:"confirm_password" validate:"required,eqfield=Password"`
    
    // Optional phone (validated if provided)
    Phone string `json:"phone" validate:"omitempty,e164"`
    
    // Enum value
    Role string `json:"role" validate:"required,oneof=user admin moderator"`
    
    // Nested struct
    Address Address `json:"address" validate:"required"`
    
    // Array with constraints
    Tags []string `json:"tags" validate:"min=1,max=10,dive,min=2,max=20"`
}

type Address struct {
    Street  string `json:"street" validate:"required"`
    City    string `json:"city" validate:"required"`
    State   string `json:"state" validate:"required,len=2,alpha"`
    ZipCode string `json:"zip_code" validate:"required,numeric,len=5"`
}

func main() {
    ctx := context.Background()
    
    req := CreateUserRequest{
        Username:        "ab",                // Too short
        Email:           "invalid",           // Invalid email
        Age:             15,                  // Too young
        Password:        "pass",              // Too short
        ConfirmPassword: "different",         // Doesn't match
        Phone:           "123",               // Invalid format
        Role:            "superuser",         // Not in enum
        Address:         Address{},           // Missing required fields
        Tags:            []string{"a", "bb"}, // First tag too short
    }
    
    err := validation.Validate(ctx, &req)
    if err != nil {
        var verr *validation.Error
        if errors.As(err, &verr) {
            for _, fieldErr := range verr.Fields {
                fmt.Printf("%s: %s\n", fieldErr.Path, fieldErr.Message)
            }
        }
    }
}

Tag Reference

For a complete list of available tags, see the go-playground/validator documentation.

Common categories:

  • Comparison: eq, ne, gt, gte, lt, lte, min, max, len
  • Strings: alpha, alphanum, numeric, email, url, uuid, contains, startswith, endswith
  • Numbers: Range validation, divisibility
  • Network: ip, ipv4, ipv6, hostname, mac
  • Files: file, dir, filepath
  • Cross-field: eqfield, nefield, gtfield, ltfield
  • Conditional: required_if, required_unless, required_with, required_without

Performance Considerations

  • Struct validation tags are cached after first use (fast)
  • Tag validator is initialized lazily (only when needed)
  • Thread-safe for concurrent validation
  • No runtime overhead for unused tags

Next Steps