Form Validators
Overview
Form Validators are responsible for validating user input data submitted through forms. They ensure data integrity, enforce business rules, and provide structured error feedback before data reaches the business logic layer.
Purpose
Form Validators serve several key functions: - Input Validation - Verify form data meets required criteria - Error Collection - Gather and structure validation errors - Business Rule Enforcement - Apply domain-specific validation rules - Data Integrity - Ensure data consistency before processing
Structure
Each form validator typically follows this pattern:
type CreatePlantFormValidator struct {
Validator *validator.Validator
}
func (this *CreatePlantFormValidator) Validate(form *forms.CreatePlantForm) (*formLib.FormBag, error) {
bag := this.Validator.MakeFormBag()
// Perform validations
this.validateName(form, bag)
this.validateSpecies(form, bag)
this.validateSize(form, bag)
if !this.Validator.IsValid(bag) {
return bag, this.Validator.NewInvalidFormError()
}
return bag, nil
}
Common Validation Patterns
Required Field Validation
func (this *CreatePlantFormValidator) validateName(form *forms.CreatePlantForm, bag *formLib.FormBag) {
if strings.TrimSpace(form.Name) == "" {
this.Validator.AddError(bag, "name", "Name is required")
}
}
String Length Validation
func (this *CreatePlantFormValidator) validateDescription(form *forms.CreatePlantForm, bag *formLib.FormBag) {
desc := strings.TrimSpace(form.Description)
if len(desc) > 500 {
this.Validator.AddError(bag, "description", "Description cannot exceed 500 characters")
}
if len(desc) < 10 {
this.Validator.AddError(bag, "description", "Description must be at least 10 characters")
}
}
Format Validation
func (this *CreateUserFormValidator) validateEmail(form *forms.CreateUserForm, bag *formLib.FormBag) {
email := strings.TrimSpace(form.Email)
if email == "" {
this.Validator.AddError(bag, "email", "Email is required")
return
}
emailRegex := `^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`
if matched, _ := regexp.MatchString(emailRegex, email); !matched {
this.Validator.AddError(bag, "email", "Invalid email format")
}
}
Numeric Range Validation
func (this *CreatePlantFormValidator) validateSize(form *forms.CreatePlantForm, bag *formLib.FormBag) {
if form.Size < 0 {
this.Validator.AddError(bag, "size", "Size cannot be negative")
}
if form.Size > 1000 {
this.Validator.AddError(bag, "size", "Size cannot exceed 1000")
}
}
Boolean Validation
func (this *CreatePlantFormValidator) validatePerennial(form *forms.CreatePlantForm, bag *formLib.FormBag) {
// Boolean validation is typically handled by form binding
// Custom logic can be added for business rules
if form.Perennial && form.Size < 20 {
this.Validator.AddError(bag, "perennial", "Perennial plants must be at least 20cm")
}
}
Advanced Validation Patterns
Cross-Field Validation
func (this *CreatePlantFormValidator) validatePlantingDates(form *forms.CreatePlantForm, bag *formLib.FormBag) {
if !form.PlantedAt.IsZero() && !form.HarvestedAt.IsZero() {
if form.HarvestedAt.Before(form.PlantedAt) {
this.Validator.AddError(bag, "harvested_at", "Harvest date cannot be before planting date")
}
}
}
Conditional Validation
func (this *CreatePlantFormValidator) validateHarvestData(form *forms.CreatePlantForm, bag *formLib.FormBag) {
if form.Edible {
if form.HarvestSeason == "" {
this.Validator.AddError(bag, "harvest_season", "Harvest season is required for edible plants")
}
if form.DaysToHarvest <= 0 {
this.Validator.AddError(bag, "days_to_harvest", "Days to harvest must be specified for edible plants")
}
}
}
Database Validation (with dependencies)
type CreatePlantFormValidator struct {
Validator *validator.Validator
PlantFetcher *fetchers.PlantFetcher
}
func (this *CreatePlantFormValidator) validateUniqueName(ctx context.Context, form *forms.CreatePlantForm, bag *formLib.FormBag) {
if form.Name == "" {
return // Skip if name is empty (handled by required validation)
}
existing, exists, err := this.PlantFetcher.FindOneByName(ctx, form.Name)
if err != nil {
this.Validator.AddError(bag, "name", "Unable to validate plant name")
return
}
if exists {
this.Validator.AddError(bag, "name", "Plant name already exists")
}
}
Integration with Controllers
Form validators are typically used in controller methods:
func (gc *GardenController) Create(c *gin.Context) {
form := &forms.CreateGardenForm{}
// Bind form data
if err := c.ShouldBind(form); err != nil {
c.JSON(400, gin.H{"error": "Invalid form data"})
return
}
// Validate form
bag, err := gc.CreateGardenFormValidator.Validate(form)
if err != nil {
c.JSON(400, gin.H{
"error": "Validation failed",
"errors": bag.Errors,
})
return
}
// Proceed with business logic...
}
Error Handling
Error Structure
type FormBag struct {
Errors map[string][]string
Valid bool
}
// Add error to specific field
func (v *Validator) AddError(bag *FormBag, field, message string) {
if bag.Errors == nil {
bag.Errors = make(map[string][]string)
}
bag.Errors[field] = append(bag.Errors[field], message)
bag.Valid = false
}
Multiple Errors per Field
func (this *CreateUserFormValidator) validatePassword(form *forms.CreateUserForm, bag *formLib.FormBag) {
password := form.Password
if len(password) < 8 {
this.Validator.AddError(bag, "password", "Password must be at least 8 characters")
}
if !strings.ContainsAny(password, "0123456789") {
this.Validator.AddError(bag, "password", "Password must contain at least one number")
}
if !strings.ContainsAny(password, "ABCDEFGHIJKLMNOPQRSTUVWXYZ") {
this.Validator.AddError(bag, "password", "Password must contain at least one uppercase letter")
}
}
Custom Validation Rules
Reusable Validation Functions
func (v *Validator) ValidateRequiredString(bag *formLib.FormBag, field, value, message string) {
if strings.TrimSpace(value) == "" {
v.AddError(bag, field, message)
}
}
func (v *Validator) ValidateStringLength(bag *formLib.FormBag, field, value string, min, max int) {
length := len(strings.TrimSpace(value))
if length < min {
v.AddError(bag, field, fmt.Sprintf("Must be at least %d characters", min))
}
if length > max {
v.AddError(bag, field, fmt.Sprintf("Cannot exceed %d characters", max))
}
}
Using Custom Rules
func (this *CreatePlantFormValidator) Validate(form *forms.CreatePlantForm) (*formLib.FormBag, error) {
bag := this.Validator.MakeFormBag()
this.Validator.ValidateRequiredString(bag, "name", form.Name, "Plant name is required")
this.Validator.ValidateStringLength(bag, "name", form.Name, 2, 100)
this.Validator.ValidateStringLength(bag, "description", form.Description, 0, 500)
if !this.Validator.IsValid(bag) {
return bag, this.Validator.NewInvalidFormError()
}
return bag, nil
}
Testing Form Validators
func TestCreatePlantFormValidator_ValidForm(t *testing.T) {
validator := setupCreatePlantFormValidator()
form := &forms.CreatePlantForm{
Name: "Tomato",
Species: "Solanum lycopersicum",
Size: 50,
Perennial: false,
}
bag, err := validator.Validate(form)
assert.NoError(t, err)
assert.True(t, bag.Valid)
assert.Empty(t, bag.Errors)
}
func TestCreatePlantFormValidator_InvalidForm(t *testing.T) {
validator := setupCreatePlantFormValidator()
form := &forms.CreatePlantForm{
Name: "", // Invalid - required
Size: -5, // Invalid - negative
}
bag, err := validator.Validate(form)
assert.Error(t, err)
assert.False(t, bag.Valid)
assert.Contains(t, bag.Errors, "name")
assert.Contains(t, bag.Errors, "size")
}
Best Practices
Validation Order
- Validate required fields first
- Perform format validation before business rules
- Check database constraints last
Error Messages
- Provide clear, actionable error messages
- Use consistent language across validators
- Avoid exposing internal implementation details
Performance
- Validate cheapest rules first (fail fast)
- Avoid expensive database calls when possible
- Cache validation results when appropriate
Maintainability
- Use consistent validation patterns
- Extract common validation logic into reusable functions
- Keep validators focused on single forms
Form Validators ensure data quality and provide user-friendly error feedback, forming a crucial layer in the application’s data validation strategy.