Custom Validator
Custom Validator
Section titled “Custom Validator”This example demonstrates how to implement custom validation logic beyond standard struct tag validation using the IsValider interface.
Features
Section titled “Features”- Custom password strength validation
- Username format and reserved words validation
- Email domain whitelist validation
- Content profanity checking
- Tag format validation
- Custom error messages with error codes
Complete Example
Section titled “Complete Example”package main
import ( "errors" "net/http" "regexp" "strings"
"github.com/fox-gonic/fox" "github.com/fox-gonic/fox/httperrors")
// StrongPassword validates password strengthtype StrongPassword struct { Password string `json:"password" binding:"required"`}
func (sp *StrongPassword) IsValid() error { pwd := sp.Password
// Check minimum length if len(pwd) < 8 { return &httperrors.Error{ HTTPCode: http.StatusBadRequest, Code: "PASSWORD_TOO_SHORT", Err: errors.New("password must be at least 8 characters long"), } }
// Check for uppercase if !regexp.MustCompile(`[A-Z]`).MatchString(pwd) { return &httperrors.Error{ HTTPCode: http.StatusBadRequest, Code: "PASSWORD_NO_UPPERCASE", Err: errors.New("password must contain at least one uppercase letter"), } }
// Check for lowercase if !regexp.MustCompile(`[a-z]`).MatchString(pwd) { return &httperrors.Error{ HTTPCode: http.StatusBadRequest, Code: "PASSWORD_NO_LOWERCASE", Err: errors.New("password must contain at least one lowercase letter"), } }
// Check for digit if !regexp.MustCompile(`[0-9]`).MatchString(pwd) { return &httperrors.Error{ HTTPCode: http.StatusBadRequest, Code: "PASSWORD_NO_DIGIT", Err: errors.New("password must contain at least one digit"), } }
// Check for special character if !regexp.MustCompile(`[!@#$%^&*(),.?":{}|<>]`).MatchString(pwd) { return &httperrors.Error{ HTTPCode: http.StatusBadRequest, Code: "PASSWORD_NO_SPECIAL", Err: errors.New("password must contain at least one special character"), } }
return nil}
// SignupRequest with custom validationtype SignupRequest struct { Username string `json:"username" binding:"required,min=3,max=50"` Email string `json:"email" binding:"required,email"` Password string `json:"password" binding:"required"`}
func (sr *SignupRequest) IsValid() error { // Username validation if !regexp.MustCompile(`^[a-zA-Z0-9_-]+$`).MatchString(sr.Username) { return &httperrors.Error{ HTTPCode: http.StatusBadRequest, Code: "INVALID_USERNAME", Err: errors.New("username can only contain letters, numbers, underscore, and dash"), } }
// Reserved usernames reserved := []string{"admin", "root", "system", "api", "www"} for _, r := range reserved { if strings.EqualFold(sr.Username, r) { return &httperrors.Error{ HTTPCode: http.StatusBadRequest, Code: "RESERVED_USERNAME", Err: errors.New("this username is reserved"), } } }
// Email domain validation allowedDomains := []string{"example.com", "test.com", "demo.com"} emailParts := strings.Split(sr.Email, "@") if len(emailParts) == 2 { domain := emailParts[1] valid := false for _, d := range allowedDomains { if domain == d { valid = true break } } if !valid { return &httperrors.Error{ HTTPCode: http.StatusBadRequest, Code: "INVALID_EMAIL_DOMAIN", Err: errors.New("email domain not allowed. Use: " + strings.Join(allowedDomains, ", ")), } } }
// Password validation pwdReq := &StrongPassword{Password: sr.Password} return pwdReq.IsValid()}
// CreatePostRequest with content validationtype CreatePostRequest struct { Title string `json:"title" binding:"required,min=5,max=200"` Content string `json:"content" binding:"required,min=10"` Tags []string `json:"tags" binding:"required,min=1,max=10"`}
func (cpr *CreatePostRequest) IsValid() error { // Check for profanity in title profanityWords := []string{"badword1", "badword2"} titleLower := strings.ToLower(cpr.Title) for _, word := range profanityWords { if strings.Contains(titleLower, word) { return &httperrors.Error{ HTTPCode: http.StatusBadRequest, Code: "PROFANITY_DETECTED", Err: errors.New("title contains inappropriate content"), } } }
// Validate tags for _, tag := range cpr.Tags { if len(tag) < 2 || len(tag) > 30 { return &httperrors.Error{ HTTPCode: http.StatusBadRequest, Code: "INVALID_TAG_LENGTH", Err: errors.New("each tag must be between 2 and 30 characters"), } }
if !regexp.MustCompile(`^[a-zA-Z0-9-]+$`).MatchString(tag) { return &httperrors.Error{ HTTPCode: http.StatusBadRequest, Code: "INVALID_TAG_FORMAT", Err: errors.New("tags can only contain letters, numbers, and dashes"), } } }
return nil}
func main() { router := fox.New()
// Password validation endpoint router.POST("/validate-password", func(_ *fox.Context, _ *StrongPassword) (string, error) { return "Password is strong!", nil })
// Signup with comprehensive validation router.POST("/signup", func(_ *fox.Context, req *SignupRequest) (map[string]any, error) { return map[string]any{ "message": "Account created successfully", "username": req.Username, "email": req.Email, }, nil })
// Create post with content validation router.POST("/posts", func(_ *fox.Context, req *CreatePostRequest) (map[string]any, error) { return map[string]any{ "message": "Post created successfully", "post": map[string]any{ "title": req.Title, "content": req.Content, "tags": req.Tags, }, }, nil })
if err := router.Run(":8080"); err != nil { panic(err) }}Running the Example
Section titled “Running the Example”go run main.goTesting
Section titled “Testing”Valid Password
Section titled “Valid Password”curl -X POST http://localhost:8080/validate-password \ -H "Content-Type: application/json" \ -d '{"password": "StrongPass123!"}'Weak Password (No Uppercase)
Section titled “Weak Password (No Uppercase)”curl -X POST http://localhost:8080/validate-password \ -H "Content-Type: application/json" \ -d '{"password": "weakpass123!"}'Valid Signup
Section titled “Valid Signup”curl -X POST http://localhost:8080/signup \ -H "Content-Type: application/json" \ -d '{ "username": "john_doe", "email": "john@example.com", "password": "SecurePass123!" }'Invalid Username (Reserved)
Section titled “Invalid Username (Reserved)”curl -X POST http://localhost:8080/signup \ -H "Content-Type: application/json" \ -d '{ "username": "admin", "email": "admin@example.com", "password": "SecurePass123!" }'Invalid Email Domain
Section titled “Invalid Email Domain”curl -X POST http://localhost:8080/signup \ -H "Content-Type: application/json" \ -d '{ "username": "john_doe", "email": "john@gmail.com", "password": "SecurePass123!" }'Valid Post Creation
Section titled “Valid Post Creation”curl -X POST http://localhost:8080/posts \ -H "Content-Type: application/json" \ -d '{ "title": "My First Post", "content": "This is the content of my post.", "tags": ["golang", "web-development", "fox"] }'IsValider Interface
Section titled “IsValider Interface”Fox provides an IsValider interface:
type IsValider interface { IsValid() error}Any struct that implements this interface will have its IsValid() method called automatically after standard validation passes.
Error Response Format
Section titled “Error Response Format”Custom validators should return *httperrors.Error with:
&httperrors.Error{ HTTPCode: http.StatusBadRequest, // HTTP status code Code: "ERROR_CODE", // Application error code Err: errors.New("message"), // Error message}Validation Flow
Section titled “Validation Flow”Request → Parse JSON → Validate Tags → IsValid() → Handler- Parse JSON: Parse request body
- Validate Tags: Run struct tag validation (
required,email, etc.) - IsValid(): If struct implements
IsValider, callIsValid() - Handler: If all validation passes, call handler
Best Practices
Section titled “Best Practices”- Fail Fast: Return error as soon as first validation fails
- Clear Messages: Provide user-friendly error messages
- Error Codes: Use consistent error codes (UPPER_SNAKE_CASE)
- Security: Don’t expose internal implementation details
- Performance: Cache compiled regex patterns
- Reusability: Extract common validation logic into separate structs
Next Steps
Section titled “Next Steps”- Error Handling - Handle validation errors
- Binding - Parameter binding basics
- Validation Documentation - Learn more about validation