Skip to Content
Go Realm v1 is released 🎉

Part 1: Advanced Validations

Learn advanced validation techniques including dates, custom rules, enums, conditional, and cross-field validations.


Step 10: Date & Time Validations {#step10-dates}

📝 Create examples/date_validations.go:

package main import ( "fmt" "time" "github.com/go-playground/validator/v10" ) func main() { validate := validator.New() fmt.Println("=== Date & Time Validations ===\n") type DateDTO struct { // Required date BirthDate time.Time `validate:"required"` // Event date must be after birth date EventDate time.Time `validate:"required,gtefield=BirthDate"` // Date string in specific format DateString string `validate:"required,datetime=2006-01-02"` // RFC3339 datetime CreatedAt time.Time `validate:"required"` // Optional updated time UpdatedAt *time.Time `validate:"omitempty"` // Time must be before now ExpiryDate time.Time `validate:"required"` } // Valid example fmt.Println("Test 1: Valid dates") now := time.Now() valid := DateDTO{ BirthDate: time.Date(1990, 1, 1, 0, 0, 0, 0, time.UTC), EventDate: time.Date(2024, 6, 15, 0, 0, 0, 0, time.UTC), DateString: "2024-01-15", CreatedAt: now, UpdatedAt: &now, ExpiryDate: time.Date(2025, 12, 31, 23, 59, 59, 0, time.UTC), } err := validate.Struct(valid) if err == nil { fmt.Println("✅ All date validations passed") fmt.Printf(" Birth: %s\n", valid.BirthDate.Format("2006-01-02")) fmt.Printf(" Event: %s\n", valid.EventDate.Format("2006-01-02")) fmt.Printf(" Created: %s\n", valid.CreatedAt.Format("2006-01-02 15:04:05")) } fmt.Println() // Invalid example fmt.Println("Test 2: Invalid dates") invalid := DateDTO{ BirthDate: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC), EventDate: time.Date(1990, 1, 1, 0, 0, 0, 0, time.UTC), // Before birth DateString: "2024/01/15", // Wrong format CreatedAt: time.Time{}, // Zero time UpdatedAt: nil, ExpiryDate: time.Time{}, } err = validate.Struct(invalid) if err != nil { fmt.Println("❌ Validation failed:") for _, err := range err.(validator.ValidationErrors) { fmt.Printf(" - %s failed '%s'\n", err.Field(), err.Tag()) } } fmt.Println() // Date comparison validations fmt.Println("=== Date Comparison Validations ===\n") type DateRangeDTO struct { StartDate time.Time `validate:"required"` EndDate time.Time `validate:"required,gtefield=StartDate"` // Date must be in the past HistoricalDate time.Time `validate:"required,ltefield=CurrentTime"` CurrentTime time.Time `validate:"required"` // Date must be in the future FutureEvent time.Time `validate:"required,gtefield=CurrentTime"` } fmt.Println("Test 3: Valid date ranges") currentTime := time.Now() validRange := DateRangeDTO{ StartDate: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC), EndDate: time.Date(2024, 12, 31, 0, 0, 0, 0, time.UTC), HistoricalDate: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC), CurrentTime: currentTime, FutureEvent: currentTime.Add(24 * time.Hour), } err = validate.Struct(validRange) if err == nil { fmt.Println("✅ Date range validations passed") } fmt.Println() fmt.Println("Test 4: Invalid date ranges") invalidRange := DateRangeDTO{ StartDate: time.Date(2024, 12, 31, 0, 0, 0, 0, time.UTC), EndDate: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC), // Before start HistoricalDate: currentTime.Add(24 * time.Hour), // In future CurrentTime: currentTime, FutureEvent: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC), // In past } err = validate.Struct(invalidRange) if err != nil { fmt.Println("❌ Validation failed:") for _, err := range err.(validator.ValidationErrors) { fmt.Printf(" - %s failed '%s'\n", err.Field(), err.Tag()) } } }

Key Date Validations:

ValidationTagExample
Date formatdatetime=2006-01-02”2024-01-15”
After another dategtefield=StartDateEndDate > StartDate
Before another dateltefield=EndDateStartDate < EndDate
Required daterequiredMust have value

Step 11: File Validations {#step11-files}

File validations are useful for handling file paths and extensions.

Common File Validations:

ValidationTagUse Case
File existsfilePath to existing file
File path formatfilepathValid file path format
Directory pathdirpathValid directory path
File extensionendswith=.jpgCheck file extension

Example:

type FileUploadDTO struct { ImagePath string `validate:"required,endswith=.jpg"` DataPath string `validate:"required,filepath"` }

Step 12: Custom Validations {#step12-custom}

12.1 Validating WITHOUT Structs

You don’t always need structs! Use validate.Var() to validate individual values:

📝 Create examples/custom_validation_demo.go:

package main import ( "fmt" "regexp" "github.com/go-playground/validator/v10" ) func main() { validate := validator.New() // 1. Validate single values directly email := "user@example.com" err := validate.Var(email, "required,email") if err == nil { fmt.Println("✅ Valid email") } // 2. Register custom validation validate.RegisterValidation("strong_password", func(fl validator.FieldLevel) bool { password := fl.Field().String() hasUpper := regexp.MustCompile(`[A-Z]`).MatchString(password) hasLower := regexp.MustCompile(`[a-z]`).MatchString(password) hasNumber := regexp.MustCompile(`[0-9]`).MatchString(password) hasSpecial := regexp.MustCompile(`[!@#$%^&*]`).MatchString(password) return len(password) >= 8 && hasUpper && hasLower && hasNumber && hasSpecial }) password := "MyP@ss123" err = validate.Var(password, "strong_password") if err == nil { fmt.Println("✅ Strong password") } // 3. Validate slices emails := []string{"user1@example.com", "user2@example.com"} err = validate.Var(emails, "required,dive,email") // 4. Validate maps userAges := map[string]int{"john": 25, "jane": 30} err = validate.Var(userAges, "dive,keys,alpha,endkeys,gte=18") // 5. Cross-value validation without struct password1 := "mypassword" password2 := "mypassword" err = validate.VarWithValue(password1, password2, "eqfield") if err == nil { fmt.Println("✅ Passwords match") } }

Run:

go run examples/custom_validation_demo.go

Key Methods:

  • validate.Var(value, "rules") - Validate single value
  • validate.VarWithValue(val1, val2, "rule") - Compare two values
  • validate.RegisterValidation("name", func) - Add custom rule

Step 13: Enum Validations {#step13-enum}

13.1 Enum with oneof Tag

The oneof tag validates that a value is one of a predefined set.

📝 Create examples/enum_demo.go:

package main import ( "fmt" "github.com/go-playground/validator/v10" ) func main() { validate := validator.New() fmt.Println("=== Enum Validations ===\n") type UserDTO struct { // String enum Role string `validate:"required,oneof=admin user guest moderator"` // Number enum Priority int `validate:"required,oneof=1 2 3 4 5"` // Status enum Status string `validate:"required,oneof=active inactive pending suspended"` // Case-sensitive enum Environment string `validate:"required,oneof=development staging production"` // Multiple word enum (space-separated in tag) Department string `validate:"omitempty,oneof=engineering marketing sales hr"` } // Valid example fmt.Println("Test 1: Valid enum values") valid := UserDTO{ Role: "admin", Priority: 1, Status: "active", Environment: "production", Department: "engineering", } err := validate.Struct(valid) if err == nil { fmt.Println("✅ All enum validations passed") fmt.Printf(" Role: %s, Status: %s, Priority: %d\n", valid.Role, valid.Status, valid.Priority) } fmt.Println() // Invalid example fmt.Println("Test 2: Invalid enum values") invalid := UserDTO{ Role: "superadmin", // Not in enum Priority: 10, // Not in 1-5 Status: "deleted", // Not in enum Environment: "PRODUCTION", // Case mismatch Department: "finance", // Not in enum } err = validate.Struct(invalid) if err != nil { fmt.Println("❌ Validation failed:") for _, err := range err.(validator.ValidationErrors) { fmt.Printf(" - %s: must be one of [%s], got '%v'\n", err.Field(), err.Param(), err.Value()) } } fmt.Println() // Enum with type aliases (recommended pattern) fmt.Println("=== Type-Safe Enums (Best Practice) ===\n") type UserRole string const ( RoleAdmin UserRole = "admin" RoleUser UserRole = "user" RoleGuest UserRole = "guest" RoleModerator UserRole = "moderator" ) type TypeSafeUserDTO struct { Role UserRole `validate:"required,oneof=admin user guest moderator"` Status string `validate:"required,oneof=active inactive"` } fmt.Println("Test 3: Type-safe enum") typeSafeValid := TypeSafeUserDTO{ Role: RoleAdmin, Status: "active", } err = validate.Struct(typeSafeValid) if err == nil { fmt.Println("✅ Type-safe enum validation passed") fmt.Printf(" Role: %s (type: UserRole)\n", typeSafeValid.Role) } }

Run:

go run examples/enum_demo.go

Step 14: Conditional Validations {#step14-conditional}

14.1 Required If, Unless, With, Without

Conditional validations change rules based on other field values.

📝 Create examples/conditional_demo.go:

package main import ( "fmt" "github.com/go-playground/validator/v10" ) func main() { validate := validator.New() fmt.Println("=== Conditional Validations ===\n") type ShippingDTO struct { // Required if ShipToDifferentAddress is true ShipToDifferentAddress bool `validate:"boolean"` ShippingName string `validate:"required_if=ShipToDifferentAddress true"` ShippingAddress string `validate:"required_if=ShipToDifferentAddress true"` // Required unless PaymentMethod is 'cash' PaymentMethod string `validate:"required,oneof=card cash paypal"` CardNumber string `validate:"required_unless=PaymentMethod cash"` // Required with another field Country string `validate:"omitempty"` PostalCode string `validate:"required_with=Country"` // Required without another field Phone string `validate:"omitempty"` Email string `validate:"required_without=Phone,email"` } // Test 1: Ship to different address (valid) fmt.Println("Test 1: Ship to different address") valid1 := ShippingDTO{ ShipToDifferentAddress: true, ShippingName: "John Doe", ShippingAddress: "123 Main St", PaymentMethod: "card", CardNumber: "4111111111111111", Country: "US", PostalCode: "12345", Phone: "+1234567890", Email: "john@example.com", } err := validate.Struct(valid1) if err == nil { fmt.Println("✅ Validation passed (shipping to different address)") } fmt.Println() // Test 2: Same address, cash payment (valid) fmt.Println("Test 2: Same address, cash payment") valid2 := ShippingDTO{ ShipToDifferentAddress: false, ShippingName: "", // Not required ShippingAddress: "", // Not required PaymentMethod: "cash", CardNumber: "", // Not required for cash Country: "", PostalCode: "", Phone: "+1234567890", Email: "", // Not required when phone provided } err = validate.Struct(valid2) if err == nil { fmt.Println("✅ Validation passed (same address, cash payment)") } fmt.Println() // Test 3: Missing required conditional fields fmt.Println("Test 3: Missing required conditional fields") invalid := ShippingDTO{ ShipToDifferentAddress: true, ShippingName: "", // Required but missing ShippingAddress: "", // Required but missing PaymentMethod: "card", CardNumber: "", // Required but missing Country: "US", PostalCode: "", // Required with Country Phone: "", Email: "", // Required without Phone } err = validate.Struct(invalid) if err != nil { fmt.Println("❌ Validation failed:") for _, err := range err.(validator.ValidationErrors) { fmt.Printf(" - %s failed '%s'\n", err.Field(), err.Tag()) } } }

Step 15: Cross-Field Validations {#step15-cross-field}

15.1 Field Comparisons

Compare one field’s value against another field.

📝 Create examples/cross_field_demo.go:

package main import ( "fmt" "time" "github.com/go-playground/validator/v10" ) func main() { validate := validator.New() fmt.Println("=== Cross-Field Validations ===\n") type PasswordDTO struct { Password string `validate:"required,min=8"` ConfirmPassword string `validate:"required,eqfield=Password"` } // Test 1: Matching passwords fmt.Println("Test 1: Matching passwords") validPass := PasswordDTO{ Password: "securepass123", ConfirmPassword: "securepass123", } err := validate.Struct(validPass) if err == nil { fmt.Println("✅ Passwords match") } fmt.Println() // Test 2: Non-matching passwords fmt.Println("Test 2: Non-matching passwords") invalidPass := PasswordDTO{ Password: "securepass123", ConfirmPassword: "different", } err = validate.Struct(invalidPass) if err != nil { fmt.Println("❌ Validation failed:") for _, err := range err.(validator.ValidationErrors) { fmt.Printf(" - %s: must equal %s\n", err.Field(), err.Param()) } } fmt.Println() // Advanced cross-field validations fmt.Println("=== Advanced Cross-Field Validations ===\n") type RangeDTO struct { MinPrice float64 `validate:"required,gte=0"` MaxPrice float64 `validate:"required,gtefield=MinPrice"` StartDate time.Time `validate:"required"` EndDate time.Time `validate:"required,gtefield=StartDate"` MinAge int `validate:"required,gte=0"` MaxAge int `validate:"required,gtefield=MinAge,lte=120"` // Not equal to another field NewEmail string `validate:"required,email"` OldEmail string `validate:"required,email"` EmailChanged bool `validate:"omitempty"` } fmt.Println("Test 3: Valid ranges") validRange := RangeDTO{ MinPrice: 10.00, MaxPrice: 100.00, StartDate: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC), EndDate: time.Date(2024, 12, 31, 0, 0, 0, 0, time.UTC), MinAge: 18, MaxAge: 65, NewEmail: "new@example.com", OldEmail: "old@example.com", } err = validate.Struct(validRange) if err == nil { fmt.Println("✅ All range validations passed") fmt.Printf(" Price range: $%.2f - $%.2f\n", validRange.MinPrice, validRange.MaxPrice) fmt.Printf(" Age range: %d - %d\n", validRange.MinAge, validRange.MaxAge) } fmt.Println() fmt.Println("Test 4: Invalid ranges") invalidRange := RangeDTO{ MinPrice: 100.00, MaxPrice: 10.00, // Less than min StartDate: time.Date(2024, 12, 31, 0, 0, 0, 0, time.UTC), EndDate: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC), // Before start MinAge: 65, MaxAge: 18, // Less than min NewEmail: "email@example.com", OldEmail: "email@example.com", // Should be different } err = validate.Struct(invalidRange) if err != nil { fmt.Println("❌ Validation failed:") for _, err := range err.(validator.ValidationErrors) { fmt.Printf(" - %s failed '%s'\n", err.Field(), err.Tag()) } } fmt.Println() // Field comparison operators fmt.Println("=== Field Comparison Operators ===\n") type ComparisonDTO struct { Value1 int `validate:"required"` Value2 int `validate:"required,eqfield=Value1"` // Equal Value3 int `validate:"required,nefield=Value1"` // Not equal Value4 int `validate:"required,gtfield=Value1"` // Greater than Value5 int `validate:"required,gtefield=Value1"` // Greater than or equal Value6 int `validate:"required,ltfield=Value4"` // Less than Value7 int `validate:"required,ltefield=Value4"` // Less than or equal } fmt.Println("Test 5: Field comparisons") validComp := ComparisonDTO{ Value1: 10, Value2: 10, // Equal to Value1 Value3: 20, // Not equal to Value1 Value4: 30, // Greater than Value1 Value5: 15, // Greater than or equal to Value1 Value6: 25, // Less than Value4 Value7: 30, // Less than or equal to Value4 } err = validate.Struct(validComp) if err == nil { fmt.Println("✅ All field comparison validations passed") } else { fmt.Println("❌ Validation failed:") for _, err := range err.(validator.ValidationErrors) { fmt.Printf(" - %s failed '%s=%s'\n", err.Field(), err.Tag(), err.Param()) } } }

Key Cross-Field Validations:

ValidationTagExample
Equal to fieldeqfield=PasswordConfirm password
Not equal to fieldnefield=OldEmailNew email must differ
Greater than fieldgtfield=MinPriceMax > Min
Greater or equal fieldgtefield=StartDateEnd >= Start
Less than fieldltfield=MaxPriceMin < Max
Less or equal fieldltefield=EndDateStart <= End