Skip to Content
Go Realm v1 is released 🎉
Go DTO Validator/v10Part 1: Hands-On LearningSetup & Basics

Part 1: Setup & Basics

How to Use This Section: Follow each step in order. Copy the code, paste it, run it, see the output. Each step builds on the previous one incrementally.


Step 1: Project Setup {#step1-setup}

1.1 Create Project

mkdir go-dto-validator cd go-dto-validator go mod init github.com/TonmoyTalukder/go-dto-validator

1.2 Install Validator

go get github.com/go-playground/validator/v10

🔍 What is validator/v10?

What is it?

  • A powerful Go validation library for validating structs and fields
  • Used to validate DTOs before they reach your business logic or database

What Does It Do?

  • Validates structs: Checks if struct fields meet defined criteria
  • Tag-based: Uses struct tags like validate:"required,email"
  • Built-in rules: 100+ built-in validation rules
  • Custom rules: Allows you to define custom validation logic
  • Error messages: Returns detailed validation errors

Where Does It Fit?

HTTP Request → Parse JSON → DTO → VALIDATE (validator) → Business Logic → Database (ENT)

1.3 Verify Installation

go list -m github.com/go-playground/validator/v10

✅ Expected Output: github.com/go-playground/validator/v10 v10.x.x


Step 2: First Validation (Hello World) {#step2-first-validation}

2.1 Create main.go

package main import ( "fmt" "github.com/go-playground/validator/v10" ) type UserDTO struct { Name string `validate:"required"` Email string `validate:"required,email"` Age int `validate:"gte=18"` } func main() { validate := validator.New() fmt.Println("=== Validator Hello World ===") fmt.Println() // Test case 1: Invalid data fmt.Println("Test 1: Invalid data") user1 := UserDTO{ Name: "", Email: "invalid-email", Age: 15, } err := validate.Struct(user1) if err != nil { fmt.Println("❌ Validation failed:") for _, err := range err.(validator.ValidationErrors) { fmt.Printf(" - Field: %s, Tag: %s\n", err.Field(), err.Tag()) } } else { fmt.Println("✅ Validation passed") } fmt.Println() // Test case 2: Valid data fmt.Println("Test 2: Valid data") user2 := UserDTO{ Name: "John Doe", Email: "john@example.com", Age: 25, } err = validate.Struct(user2) if err != nil { fmt.Println("❌ Validation failed") } else { fmt.Println("✅ Validation passed") fmt.Printf(" Name: %s, Email: %s, Age: %d\n", user2.Name, user2.Email, user2.Age) } }

2.2 Run It

go run .

✅ Expected Output:

=== Validator Hello World === Test 1: Invalid data ❌ Validation failed: - Field: Name, Tag: required - Field: Email, Tag: email - Field: Age, Tag: gte Test 2: Valid data ✅ Validation passed Name: John Doe, Email: john@example.com, Age: 25

🎉 Congratulations! You’ve validated your first DTO.


Step 3: Understanding Validation Tags {#step3-tags}

3.1 Tag Syntax

Validator uses struct tags with the validate key:

FieldName Type `validate:"rule1,rule2=param,rule3"`

3.2 Common Tag Patterns

PatternExampleMeaning
Single rulevalidate:"required"Field must not be empty
Multiple rulesvalidate:"required,email"Multiple validations (AND)
Rule with paramvalidate:"min=5"Minimum length 5
Multiple paramsvalidate:"min=5,max=20"Between 5 and 20
Optional validationvalidate:"omitempty,email"Validate only if not empty
Enumvalidate:"oneof=red blue green"Must be one of the values

3.3 Most Used Tags (Memorize These)

TagTypeMeaningExample
requiredAllMust have a valuevalidate:"required"
omitemptyAllSkip validation if emptyvalidate:"omitempty,email"
emailStringValid email formatvalidate:"email"
urlStringValid URL formatvalidate:"url"
minStringMinimum lengthvalidate:"min=5"
maxStringMaximum lengthvalidate:"max=20"
lenStringExact lengthvalidate:"len=10"
gteNumberGreater than or equalvalidate:"gte=18"
lteNumberLess than or equalvalidate:"lte=100"
gtNumberGreater thanvalidate:"gt=0"
ltNumberLess thanvalidate:"lt=100"
oneofAllMust be one ofvalidate:"oneof=red blue"
eqAllMust equalvalidate:"eq=admin"
neAllMust not equalvalidate:"ne=banned"

3.4 Interactive Example

📝 Add this function to main.go:

func demoTagPatterns() { validate := validator.New() fmt.Println("\n=== Understanding Tag Patterns ===") fmt.Println() type Demo struct { // Required field Username string `validate:"required"` // Optional email (only validates if provided) Email string `validate:"omitempty,email"` // String length constraints Password string `validate:"required,min=8,max=32"` // Number range Age int `validate:"gte=18,lte=120"` // Enum Role string `validate:"oneof=admin user guest"` } // Valid example valid := Demo{ Username: "john_doe", Email: "john@example.com", Password: "securepass123", Age: 25, Role: "user", } err := validate.Struct(valid) if err == nil { fmt.Println("✅ Valid data passed all validations") } // Invalid example invalid := Demo{ Username: "", // Missing required Email: "not-an-email", // Invalid email Password: "short", // Too short Age: 15, // Under 18 Role: "superadmin", // Not in enum } err = validate.Struct(invalid) if err != nil { fmt.Println("\n❌ Invalid data failed:") for _, err := range err.(validator.ValidationErrors) { fmt.Printf(" - %s failed '%s' validation\n", err.Field(), err.Tag()) } } }

Update main function:

func main() { demoTagPatterns() }

Run:

go run .

✅ Expected Output:

=== Understanding Tag Patterns === ✅ Valid data passed all validations ❌ Invalid data failed: - Username failed 'required' validation - Email failed 'email' validation - Password failed 'min' validation - Age failed 'gte' validation - Role failed 'oneof' validation

Step 4: Production Structure {#step4-structure}

4.1 Why Project Structure Matters

In production, you need:

  • Single validator instance (performance)
  • Centralized custom rules
  • Reusable error handling
  • Clean separation of concerns
go-dto-validator/ ├── go.mod ├── main.go └── internal/ └── validator/ ├── validator.go # Global instance ├── rules.go # Custom validation rules └── errors.go # Error formatting

4.3 Create internal/validator/validator.go

mkdir -p internal/validator

📝 Create internal/validator/validator.go:

package validator import ( "reflect" "strings" "sync" v10 "github.com/go-playground/validator/v10" ) var ( once sync.Once v *v10.Validate ) // V returns a singleton validator instance func V() *v10.Validate { once.Do(func() { v = v10.New() // Use JSON field names in error messages v.RegisterTagNameFunc(func(f reflect.StructField) string { name := f.Tag.Get("json") if name == "" { return f.Name } // Extract field name before comma name = strings.Split(name, ",")[0] if name == "-" { return "" } return name }) // Register custom validation rules registerCustomRules(v) }) return v }

💡 Why singleton pattern?

  • Performance: Creating a validator is expensive (reflection, rule registration)
  • Thread-safe: sync.Once ensures one-time initialization
  • Memory efficient: Single instance across your application
  • Production standard: Same pattern used by gin, echo, etc.

4.4 Create internal/validator/rules.go

📝 Create internal/validator/rules.go:

package validator import ( "regexp" v10 "github.com/go-playground/validator/v10" ) // Custom validation regex patterns var ( usernameRegex = regexp.MustCompile(`^[a-z][a-z0-9_]{2,19}$`) phoneRegex = regexp.MustCompile(`^\+?[1-9]\d{1,14}$`) ) func registerCustomRules(v *v10.Validate) { // Username: starts with letter, 3-20 chars, alphanumeric + underscore v.RegisterValidation("username", validateUsername) // Phone: E.164 format v.RegisterValidation("phone", validatePhone) } func validateUsername(fl v10.FieldLevel) bool { return usernameRegex.MatchString(fl.Field().String()) } func validatePhone(fl v10.FieldLevel) bool { return phoneRegex.MatchString(fl.Field().String()) }

4.5 Create internal/validator/errors.go

📝 Create internal/validator/errors.go:

package validator import ( "fmt" v10 "github.com/go-playground/validator/v10" ) type FieldErrors map[string]string // ToFieldErrors converts validator errors to user-friendly messages func ToFieldErrors(err error) FieldErrors { out := FieldErrors{} // Type assertion to validator.ValidationErrors validationErrors, ok := err.(v10.ValidationErrors) if !ok { out["_error"] = "invalid input format" return out } for _, fieldErr := range validationErrors { field := fieldErr.Field() tag := fieldErr.Tag() param := fieldErr.Param() out[field] = formatError(tag, param, field) } return out } func formatError(tag, param, field string) string { switch tag { case "required": return "is required" case "email": return "must be a valid email address" case "url": return "must be a valid URL" case "min": return fmt.Sprintf("must be at least %s characters", param) case "max": return fmt.Sprintf("must be at most %s characters", param) case "len": return fmt.Sprintf("must be exactly %s characters", param) case "gte": return fmt.Sprintf("must be greater than or equal to %s", param) case "lte": return fmt.Sprintf("must be less than or equal to %s", param) case "gt": return fmt.Sprintf("must be greater than %s", param) case "lt": return fmt.Sprintf("must be less than %s", param) case "oneof": return fmt.Sprintf("must be one of: %s", param) case "username": return "must be a valid username (3-20 chars, start with letter, alphanumeric + underscore)" case "phone": return "must be a valid phone number" case "unique": return "must contain unique values" case "dive": return "array element validation failed" default: return fmt.Sprintf("failed validation: %s", tag) } }

4.6 Update main.go to Use Production Structure

📝 Replace main.go with:

package main import ( "fmt" "github.com/TonmoyTalukder/go-dto-validator/internal/validator" ) type CreateUserDTO struct { Name string `json:"name" validate:"required,min=2,max=50"` Username string `json:"username" validate:"required,username"` Email string `json:"email" validate:"required,email"` Phone string `json:"phone" validate:"omitempty,phone"` Age int `json:"age" validate:"omitempty,gte=0,lte=120"` Role string `json:"role" validate:"required,oneof=admin user guest"` } func main() { fmt.Println("=== Production Validator Setup ===\n") // Test case 1: Valid data fmt.Println("Test 1: Valid data") validUser := CreateUserDTO{ Name: "John Doe", Username: "johndoe", Email: "john@example.com", Phone: "+1234567890", Age: 25, Role: "user", } err := validator.V().Struct(validUser) if err != nil { fmt.Println("❌ Validation failed:") for field, msg := range validator.ToFieldErrors(err) { fmt.Printf(" - %s: %s\n", field, msg) } } else { fmt.Println("✅ Validation passed") fmt.Printf(" User: %s (%s)\n", validUser.Name, validUser.Email) } fmt.Println() // Test case 2: Invalid data fmt.Println("Test 2: Invalid data") invalidUser := CreateUserDTO{ Name: "J", // Too short Username: "123invalid", // Doesn't start with letter Email: "not-an-email", // Invalid email Phone: "invalid-phone", // Invalid phone Age: 150, // Out of range Role: "superadmin", // Not in enum } err = validator.V().Struct(invalidUser) if err != nil { fmt.Println("❌ Validation failed:") for field, msg := range validator.ToFieldErrors(err) { fmt.Printf(" - %s: %s\n", field, msg) } } }

4.7 Run Production Setup

go run .

✅ Expected Output:

=== Production Validator Setup === Test 1: Valid data ✅ Validation passed User: John Doe (john@example.com) Test 2: Invalid data ❌ Validation failed: - name: must be at least 2 characters - username: must be a valid username (3-20 chars, start with letter, alphanumeric + underscore) - email: must be a valid email address - phone: must be a valid phone number - age: must be less than or equal to 120 - role: must be one of: admin user guest

🎉 You now have a production-ready validator setup!