main.go
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105
package main

import (
	"fmt"
	"regexp"

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

// User represents a user model with validation tags
type User struct {
	Username  string `json:"username" validate:"required,alphanum,min=3,max=20"`
	Email     string `json:"email" validate:"required,email"`
	Age       int    `json:"age" validate:"required,gte=18,lte=120"`
	Phone     string `json:"phone" validate:"required,phoneNumber"` // Custom validation
	Password  string `json:"password" validate:"required,min=8,max=72,containsany=!@#$%^&*"`
}

// Custom validation function for phone numbers
func validatePhoneNumber(fl validator.FieldLevel) bool {
	phoneRegex := regexp.MustCompile(`^\+?[0-9]{10,15}$`)
	return phoneRegex.MatchString(fl.Field().String())
}

// validateStruct performs validation on a struct and returns detailed errors
func validateStruct(s interface{}) map[string]string {
	// Initialize validator
	validate := validator.New()
	
	// Register custom validation
	if err := validate.RegisterValidation("phoneNumber", validatePhoneNumber); err != nil {
		return map[string]string{"system": fmt.Sprintf("failed to register custom validator: %v", err)}
	}

	// Perform validation
	errs := validate.Struct(s)
	if errs == nil {
		return nil
	}

	// Process validation errors into a user-friendly map
	errorMap := make(map[string]string)
	for _, err := range errs.(validator.ValidationErrors) {
		field := err.Field()
		switch err.Tag() {
		case "required":
			errorMap[field] = fmt.Sprintf("%s is a required field", field)
		case "email":
			errorMap[field] = "Invalid email format"
		case "alphanum":
			errorMap[field] = "Must contain only alphanumeric characters"
		case "min":
			errorMap[field] = fmt.Sprintf("Must be at least %s characters/value", err.Param())
		case "max":
			errorMap[field] = fmt.Sprintf("Must not exceed %s characters/value", err.Param())
		case "gte":
			errorMap[field] = fmt.Sprintf("Must be greater than or equal to %s", err.Param())
		case "lte":
			errorMap[field] = fmt.Sprintf("Must be less than or equal to %s", err.Param())
		case "containsany":
			errorMap[field] = fmt.Sprintf("Must contain at least one special character (%s)", err.Param())
		case "phoneNumber":
			errorMap[field] = "Invalid phone number format (expected +1234567890 or 1234567890)"
		default:
			errorMap[field] = fmt.Sprintf("Validation failed for rule: %s", err.Tag())
		}
	}

	return errorMap
}

func main() {
	// Example 1: Invalid user data
	invalidUser := User{
		Username: "jo!e",       // Contains special character
		Email:    "joe@example", // Invalid email
		Age:      17,           // Below minimum age
		Phone:    "12345",      // Invalid phone number
		Password: "password",   // No special characters
	}

	fmt.Println("Validation errors for invalid user:")
	errors := validateStruct(invalidUser)
	for field, msg := range errors {
		fmt.Printf("  %s: %s\n", field, msg)
	}

	// Example 2: Valid user data
	validUser := User{
		Username: "joe123",
		Email:    "[email protected]",
		Age:      25,
		Phone:    "+12345678901",
		Password: "Passw0rd!",
	}

	fmt.Println("\nValidation for valid user:")
	if errors := validateStruct(validUser); errors == nil {
		fmt.Println("  No validation errors - user data is valid!")
	} else {
		for field, msg := range errors {
			fmt.Printf("  %s: %s\n", field, msg)
		}
	}
}

How It Works

Validates a user payload with built-in tags and a custom phone validator to produce readable error lists.

Registers a custom phoneNumber rule, sets validation tags on User fields, runs validator.Struct, and collects validation errors into user-friendly strings for each field.

Key Concepts

  • 1Uses go-playground/validator tags for required, email, length, and containsany rules.
  • 2Custom phone validator enforces E.164-style numbers via a precompiled regex.
  • 3Errors are aggregated and formatted with field-specific messages.

When to Use This Pattern

  • Validating API request bodies before processing.
  • Checking configuration or CLI input before execution.
  • Ensuring user registration data meets policy requirements.

Best Practices

  • Reuse a single validator instance to keep custom rules registered.
  • Return clear validation messages rather than generic 400 errors.
  • Keep regex and custom logic fast to avoid slowing request handling.
Go Version1.16
Difficultyintermediate
Production ReadyYes
Lines of Code105