Skip to content

Latest commit

 

History

History
421 lines (334 loc) · 12.1 KB

File metadata and controls

421 lines (334 loc) · 12.1 KB

Go Validation Framework Guide

This guide explains how to use the github.com/bborbe/validation library effectively in Go projects.

Table of Contents

Overview

The github.com/bborbe/validation library provides a declarative approach to validation with composable validators. It follows these key principles:

  • Composable: Validators can be combined using logical operators
  • Named: Field names can be attached to validators for clear error messages
  • Type-safe: Works with Go's type system and generics
  • Context-aware: All validation functions accept context.Context

RULE go-validation/use-bborbe-validation-not-inline-checks (MUST)

Owner: go-quality-assistant Applies when: a Go service implements input validation via hand-rolled if/if-else chains in a Validate(ctx context.Context) error method or HTTP handler, instead of using github.com/bborbe/validation (validation.All{...} + validation.Name(...) + validation.NotEmptyString(...) etc.). Enforcement: rules/go/use-bborbe-validation-not-inline-checks.yml (mechanical first-pass — flags Validate methods containing if-statements that return an errors.* call directly) + judgment-tier LLM adjudication for single-field domain types with one simple check, or packages already importing github.com/bborbe/validation Why: Hand-rolled validation drifts: every package writes its own "is this empty?" check, error message format, field-naming convention, and short-circuiting policy. Code review can't enforce consistency across 30 services. bborbe/validation provides one composable library with named fields, structured errors, and consistent message formatting — adopting it everywhere eliminates 40+ lines of boilerplate per Validate method and produces consistent error JSON downstream consumers can parse.

Basic Patterns

Simple Field Validation

import "github.com/bborbe/validation"

func (u User) Validate(ctx context.Context) error {
    return validation.All{
        validation.Name("username", validation.NotEmptyString(u.Username)),
        validation.Name("email", validation.NotEmptyString(u.Email)),
    }.Validate(ctx)
}

Domain Type Validation

// Leverage existing Validate methods on domain types
func (r Request) Validate(ctx context.Context) error {
    return validation.All{
        validation.Name("userId", r.UserID),         // Uses UserID.Validate()
        validation.Name("category", r.Category),     // Uses Category.Validate()
        validation.Name("priority", r.Priority),     // Uses Priority.Validate()
    }.Validate(ctx)
}

Logical Operators

validation.All (AND Logic)

All validators must pass. Use for mandatory field validation:

return validation.All{
    validation.Name("username", validation.NotEmptyString(user.Username)),
    validation.Name("email", validation.NotEmptyString(user.Email)),
    validation.Name("age", validation.HasValidationFunc(func(ctx context.Context) error {
        if user.Age < 18 {
            return errors.New(ctx, "must be 18 or older")
        }
        return nil
    })),
}.Validate(ctx)

validation.Any (OR Logic)

At least one validator must pass. Use for optional-but-required scenarios:

// At least one contact method must be provided
return validation.Any{
    validation.Name("email", validation.NotEmptyString(r.Email)),
    validation.Name("phone", validation.NotEmptyString(r.Phone)),
    validation.Name("address", validation.NotEmptyString(r.Address)),
}.Validate(ctx)

Common Validators

String Validation

// Non-empty string
validation.NotEmptyString(value)

// String length constraints
validation.StringMinLength(5)
validation.StringMaxLength(100)

// Regular expression matching
validation.StringRegexp(`^[a-zA-Z0-9]+$`)

// Specific string values
validation.StringEquals("expected")

Nil/Existence Checks

validation.NotNil(pointer)
validation.Nil(pointer)
validation.True(boolValue)
validation.False(boolValue)

Custom Validation Functions

validation.HasValidationFunc(func(ctx context.Context) error {
    if customCondition {
        return errors.New(ctx, "custom validation failed")
    }
    return nil
})

Advanced Patterns

Conditional Validation

return validation.All{
    validation.Name("orderType", o.OrderType),
    validation.Name("price", validation.HasValidationFunc(func(ctx context.Context) error {
        switch o.OrderType {
        case "limit":
            if o.Price == nil {
                return errors.New(ctx, "price required for limit orders")
            }
        case "market":
            if o.Price != nil {
                return errors.New(ctx, "price not allowed for market orders")
            }
        }
        return nil
    })),
}.Validate(ctx)

Nested Struct Validation

type Project struct {
    Name        string
    Description string
    Owner       User
    Tasks       []Task
}

func (p Project) Validate(ctx context.Context) error {
    return validation.All{
        validation.Name("name", validation.NotEmptyString(p.Name)),
        validation.Name("owner", p.Owner),        // Calls User.Validate()
        validation.Name("tasks", p.Tasks),        // Calls Tasks.Validate()
    }.Validate(ctx)
}

Collection Validation

type Tasks []Task

func (t Tasks) Validate(ctx context.Context) error {
    for i, task := range t {
        if err := task.Validate(ctx); err != nil {
            return errors.Wrapf(ctx, err, "task[%d] validation failed", i)
        }
    }
    return nil
}

Domain Type Integration

Domain Type Validation

Well-designed domain types implement their own validation:

// UserType validates against available user types
func (u UserType) Validate(ctx context.Context) error {
    validTypes := []UserType{"admin", "user", "guest"}
    for _, validType := range validTypes {
        if u == validType {
            return nil
        }
    }
    return errors.Wrapf(ctx, validation.Error, "userType(%s) is invalid", u)
}

// Status validates against available statuses
func (s Status) Validate(ctx context.Context) error {
    validStatuses := []Status{"active", "inactive", "pending"}
    for _, validStatus := range validStatuses {
        if s == validStatus {
            return nil
        }
    }
    return errors.Wrapf(ctx, validation.Error, "status(%s) is invalid", s)
}

Leveraging Domain Validation

Always prefer using domain type validation over custom checks:

// GOOD: Uses domain validation
return validation.All{
    validation.Name("userType", u.UserType),    // Validates against valid user types
    validation.Name("status", u.Status),        // Validates against valid statuses
}.Validate(ctx)

// AVOID: Custom validation that duplicates domain logic
return validation.All{
    validation.Name("userType", validation.NotEmptyString(string(u.UserType))),  // Just length check
}.Validate(ctx)

Error Handling

Named Errors

Use validation.Name() to provide context in error messages:

return validation.All{
    validation.Name("username", validation.NotEmptyString(user.Username)),
    validation.Name("email", validation.NotEmptyString(user.Email)),
}.Validate(ctx)

// Error: "username: string is empty"
// Error: "email: string is empty"

Custom Error Messages

validation.HasValidationFunc(func(ctx context.Context) error {
    if user.Age < 18 {
        return errors.New(ctx, "user must be at least 18 years old")
    }
    return nil
})

Error Wrapping

if err := validator.Validate(ctx); err != nil {
    return errors.Wrapf(ctx, err, "validation failed for request")
}

Best Practices

1. Prefer Domain Type Validation

// GOOD: Uses domain type's built-in validation
validation.Name("category", u.Category)

// AVOID: Manual validation that might miss business rules
validation.Name("category", validation.NotEmptyString(string(u.Category)))

2. Use Descriptive Names

// GOOD: Clear field names
validation.Name("userIdentifier", u.UserIdentifier)
validation.Name("contactMethod", u.ContactMethod)

// AVOID: Generic or unclear names
validation.Name("field1", u.UserIdentifier)
validation.Name("type", u.ContactMethod)

3. Group Related Validations

return validation.All{
    // Required fields
    validation.Name("userID", r.UserID),
    validation.Name("action", r.Action),

    // Permission validation
    validation.Name("permissions", r.Permissions),
    validation.Name("role", r.Role),

    // Conditional validation based on action type
    validation.HasValidationFunc(func(ctx context.Context) error {
        return r.validateActionSpecificFields(ctx)
    }),
}.Validate(ctx)

4. Keep Validation Methods Simple

func (r Request) Validate(ctx context.Context) error {
    // Simple, declarative validation
    return validation.All{
        validation.Name("field1", r.Field1),
        validation.Name("field2", r.Field2),
        validation.Name("complex", validation.HasValidationFunc(r.validateComplexRules)),
    }.Validate(ctx)
}

func (r Request) validateComplexRules(ctx context.Context) error {
    // Complex logic extracted to separate method
    // ... implementation
}

5. Handle Empty vs Invalid Values

// For optional fields that must be valid when present
validation.Any{
    validation.StringEquals(""),                  // Empty is OK
    validation.Name("url", validation.StringURL(r.URL)), // OR must be valid URL
}

// For at-least-one-required scenarios
validation.Any{
    validation.Name("email", validation.NotEmptyString(r.Email)),
    validation.Name("phone", validation.NotEmptyString(r.Phone)),
}

Common Examples

Search Request Validation (OR Logic)

func (r SearchRequest) Validate(ctx context.Context) error {
    return validation.Any{
        validation.Name("name", validation.NotEmptyString(r.Name)),
        validation.Name("category", r.Category),
        validation.Name("status", r.Status),
        validation.Name("userID", r.UserID),
        validation.Name("dateRange", r.DateRange),
    }.Validate(ctx)
}

Complex Business Logic Validation

func (r CreateOrderRequest) Validate(ctx context.Context) error {
    return validation.All{
        validation.Name("customerID", r.CustomerID),
        validation.Name("productID", r.ProductID),
        validation.Name("quantity", validation.HasValidationFunc(func(ctx context.Context) error {
            if r.Quantity <= 0 {
                return errors.Errorf(ctx, "quantity must be greater than 0")
            }
            return nil
        })),
        validation.Name("price", validation.HasValidationFunc(func(ctx context.Context) error {
            if r.Price != nil && r.Price.Amount <= 0 {
                return errors.Errorf(ctx, "price must be greater than 0")
            }
            return nil
        })),
    }.Validate(ctx)
}

Nested Structure Validation

func (p Project) Validate(ctx context.Context) error {
    return validation.All{
        validation.Name("name", validation.NotEmptyString(p.Name)),
        validation.Name("owner", p.Owner),
        validation.Name("tasks", p.Tasks),
    }.Validate(ctx)
}

Integration with Error System

The validation framework integrates with the project's error handling:

import (
    "github.com/bborbe/errors"
    "github.com/bborbe/validation"
)

// Domain types use validation.Error for consistent error classification
func (s Status) Validate(ctx context.Context) error {
    validStatuses := []Status{"active", "inactive", "pending"}
    for _, validStatus := range validStatuses {
        if s == validStatus {
            return nil
        }
    }
    return errors.Wrapf(ctx, validation.Error, "status(%s) is invalid", s)
}

This guide should help you implement consistent, maintainable validation throughout your Go codebase.