This is the multi-page printable view of this section. Click here to print.

Return to the regular view of this page.

Validation Package

Complete API reference for the rivaas.dev/validation package

This is the API reference for the rivaas.dev/validation package. For learning-focused documentation, see the Validation Guide.

Package Information

Overview

The validation package provides flexible validation for Go structs with support for multiple strategies: struct tags, JSON Schema, and custom interfaces. It’s designed for web applications with features like partial validation for PATCH requests, sensitive data redaction, and detailed error reporting.

import "rivaas.dev/validation"

type User struct {
    Email string `validate:"required,email"`
    Age   int    `validate:"min=18"`
}

err := validation.Validate(ctx, &user)

Key Features

  • Multiple Validation Strategies: Struct tags, JSON Schema, custom interfaces
  • Partial Validation: PATCH request support with presence tracking
  • Thread-Safe: Safe for concurrent use
  • Security: Built-in redaction, nesting limits, memory protection
  • Structured Errors: Field-level errors with codes and metadata
  • Extensible: Custom tags, validators, and error messages

Package Architecture

graph TB
    User[User Code] --> API[Public API]:::info
    
    API --> Validate[Validate Functions]
    API --> Validator[Validator Type]
    API --> Presence[Presence Tracking]
    
    Validate --> Strategy[Strategy Selection]:::warning
    Validator --> Strategy
    
    Strategy --> Tags[Struct Tags]
    Strategy --> Schema[JSON Schema]
    Strategy --> Interface[Custom Interfaces]
    
    Tags --> Errors[Error Collection]
    Schema --> Errors
    Interface --> Errors
    
    Errors --> ErrorType[Error/FieldError]:::danger
    
    Presence --> Partial[Partial Validation]
    Partial --> Strategy
    
    classDef default fill:#F8FAF9,stroke:#1E6F5C,color:#1F2A27
    classDef info fill:#D1ECF1,stroke:#17A2B8,color:#1F2A27
    classDef warning fill:#FFF3CD,stroke:#FFC107,color:#1F2A27
    classDef danger fill:#F8D7DA,stroke:#DC3545,color:#1F2A27

Quick Navigation

API Reference

Core types, functions, and validation methods.

View →

Options

Configuration options and validator settings.

View →

Interfaces

Custom validation interfaces and providers.

View →

Strategies

Validation strategy selection and priority.

View →

Troubleshooting

Common validation issues and solutions.

View →

User Guide

Step-by-step tutorials and examples.

View →

Core API

Package-Level Functions

Simple validation without creating a validator instance:

// Validate with default configuration
func Validate(ctx context.Context, v any, opts ...Option) error

// Validate only present fields (PATCH requests)
func ValidatePartial(ctx context.Context, v any, pm PresenceMap, opts ...Option) error

// Compute which fields are present in JSON
func ComputePresence(rawJSON []byte) (PresenceMap, error)

Validator Type

Create configured validator instances for reuse:

// Create validator (returns error on invalid config)
func New(opts ...Option) (*Validator, error)

// Create validator (panics on invalid config)
func MustNew(opts ...Option) *Validator

// Validator methods
func (v *Validator) Validate(ctx context.Context, val any, opts ...Option) error
func (v *Validator) ValidatePartial(ctx context.Context, val any, pm PresenceMap, opts ...Option) error

Error Types

Structured validation errors:

// Main error type with multiple field errors
type Error struct {
    Fields    []FieldError
    Truncated bool
}

// Individual field error
type FieldError struct {
    Path    string         // JSON path (e.g., "items.2.price")
    Code    string         // Error code (e.g., "tag.required")
    Message string         // Human-readable message
    Meta    map[string]any // Additional metadata
}

Interfaces

Implement these for custom validation:

// Simple custom validation
type ValidatorInterface interface {
    Validate() error
}

// Context-aware custom validation
type ValidatorWithContext interface {
    ValidateContext(context.Context) error
}

// JSON Schema provider
type JSONSchemaProvider interface {
    JSONSchema() (id, schema string)
}

// Redactor for sensitive fields
type Redactor func(path string) bool

Validation Strategies

The package supports three strategies with automatic selection:

Priority Order:

  1. Interface methods (Validate() / ValidateContext())
  2. Struct tags (validate:"...")
  3. JSON Schema (JSONSchemaProvider)
// Automatic strategy selection
err := validation.Validate(ctx, &user)

// Explicit strategy
err := validation.Validate(ctx, &user,
    validation.WithStrategy(validation.StrategyTags),
)

// Run all strategies
err := validation.Validate(ctx, &user,
    validation.WithRunAll(true),
)

Configuration Options

Validator Creation Options

validator := validation.MustNew(
    validation.WithMaxErrors(10),              // Limit errors returned
    validation.WithMaxCachedSchemas(2048),     // Schema cache size
    validation.WithRedactor(redactorFunc),     // Redact sensitive fields
    validation.WithCustomTag("phone", phoneValidator), // Custom tag
    validation.WithMessages(messageMap),       // Custom error messages
    validation.WithMessageFunc("min", minFunc), // Dynamic messages
)

Per-Call Options

err := validator.Validate(ctx, &user,
    validation.WithStrategy(validation.StrategyTags),
    validation.WithPartial(true),
    validation.WithPresence(presenceMap),
    validation.WithMaxErrors(5),
    validation.WithCustomValidator(customFunc),
)

Usage Patterns

Basic Validation

type User struct {
    Email string `validate:"required,email"`
    Age   int    `validate:"min=18"`
}

user := User{Email: "test@example.com", Age: 25}
if err := validation.Validate(ctx, &user); err != nil {
    var verr *validation.Error
    if errors.As(err, &verr) {
        for _, fieldErr := range verr.Fields {
            fmt.Printf("%s: %s\n", fieldErr.Path, fieldErr.Message)
        }
    }
}

Partial Validation (PATCH)

rawJSON := []byte(`{"email": "new@example.com"}`)

presence, _ := validation.ComputePresence(rawJSON)
var req UpdateUserRequest
json.Unmarshal(rawJSON, &req)

err := validation.ValidatePartial(ctx, &req, presence)

Custom Validator

validator := validation.MustNew(
    validation.WithCustomTag("phone", func(fl validator.FieldLevel) bool {
        return phoneRegex.MatchString(fl.Field().String())
    }),
    validation.WithRedactor(func(path string) bool {
        return strings.Contains(path, "password")
    }),
)

err := validator.Validate(ctx, &user)

Performance Characteristics

  • First validation: ~500ns overhead for reflection
  • Subsequent validations: ~50ns overhead (cache lookup)
  • Schema caching: LRU with configurable size (default 1024)
  • Thread-safe: All operations safe for concurrent use
  • Zero allocation: Field paths cached per type

Integration

With net/http

func Handler(w http.ResponseWriter, r *http.Request) {
    var req CreateUserRequest
    json.NewDecoder(r.Body).Decode(&req)
    
    if err := validation.Validate(r.Context(), &req); err != nil {
        http.Error(w, err.Error(), http.StatusUnprocessableEntity)
        return
    }
    // Process request
}

With rivaas.dev/router

func Handler(c *router.Context) error {
    var req CreateUserRequest
    c.BindJSON(&req)
    
    if err := validation.Validate(c.Request().Context(), &req); err != nil {
        return c.JSON(http.StatusUnprocessableEntity, err)
    }
    return c.JSON(http.StatusOK, processRequest(req))
}

With rivaas.dev/app

func Handler(c *app.Context) error {
    var req CreateUserRequest
    if err := c.Bind(&req); err != nil {
        return err // Automatically validated and handled
    }
    return c.JSON(http.StatusOK, processRequest(req))
}

Version Compatibility

The validation package follows semantic versioning:

  • v1.x: Stable API, backward compatible
  • v2.x: Major changes, may require code updates

See Also


For step-by-step guides and tutorials, see the Validation Guide.

For real-world examples, see the Examples page.

1 - API Reference

Core types, functions, and methods

Complete API reference for the validation package’s core types, functions, and methods.

Package-Level Functions

Validate

func Validate(ctx context.Context, v any, opts ...Option) error

Validates a value using the default validator. Returns nil if validation passes, or *Error if validation fails.

Parameters:

  • ctx - Context passed to ValidatorWithContext implementations.
  • v - The value to validate. Typically a pointer to a struct.
  • opts - Optional per-call configuration options.

Returns:

  • nil on success.
  • *Error with field-level errors on failure.

Example:

err := validation.Validate(ctx, &user)
if err != nil {
    var verr *validation.Error
    if errors.As(err, &verr) {
        // Handle validation errors
    }
}

ValidatePartial

func ValidatePartial(ctx context.Context, v any, pm PresenceMap, opts ...Option) error

Validates only fields present in the PresenceMap. Useful for PATCH requests where only provided fields should be validated.

Parameters:

  • ctx - Context for validation.
  • v - The value to validate.
  • pm - Map of present fields.
  • opts - Optional configuration options.

Example:

presence, _ := validation.ComputePresence(rawJSON)
err := validation.ValidatePartial(ctx, &req, presence)

ComputePresence

func ComputePresence(rawJSON []byte) (PresenceMap, error)

Analyzes raw JSON and returns a map of present field paths. Used for partial validation.

Parameters:

  • rawJSON - Raw JSON bytes.

Returns:

  • PresenceMap - Map of field paths to true.
  • error - If JSON is invalid.

Example:

rawJSON := []byte(`{"email": "test@example.com", "age": 25}`)
presence, err := validation.ComputePresence(rawJSON)
// presence = {"email": true, "age": true}

Validator Type

New

func New(opts ...Option) (*Validator, error)

Creates a new Validator with the given options. Returns an error if configuration is invalid.

Parameters:

  • opts - Configuration options

Returns:

  • *Validator - Configured validator instance
  • error - If configuration is invalid

Example:

validator, err := validation.New(
    validation.WithMaxErrors(10),
    validation.WithRedactor(redactor),
)
if err != nil {
    return fmt.Errorf("failed to create validator: %w", err)
}

MustNew

func MustNew(opts ...Option) *Validator

Creates a new Validator with the given options. Panics if configuration is invalid. Use in main() or init() where panic on startup is acceptable.

Parameters:

  • opts - Configuration options

Returns:

  • *Validator - Configured validator instance

Panics:

  • If configuration is invalid

Example:

var validator = validation.MustNew(
    validation.WithMaxErrors(10),
    validation.WithRedactor(redactor),
)

Validator.Validate

func (v *Validator) Validate(ctx context.Context, val any, opts ...Option) error

Validates a value using this validator’s configuration. Per-call options override the validator’s base configuration.

Parameters:

  • ctx - Context for validation
  • val - The value to validate
  • opts - Optional per-call configuration overrides

Returns:

  • nil on success
  • *Error on failure

Example:

err := validator.Validate(ctx, &user,
    validation.WithMaxErrors(5), // Override base config
)

Validator.ValidatePartial

func (v *Validator) ValidatePartial(ctx context.Context, val any, pm PresenceMap, opts ...Option) error

Validates only fields present in the PresenceMap using this validator’s configuration.

Parameters:

  • ctx - Context for validation
  • val - The value to validate
  • pm - Map of present fields
  • opts - Optional configuration overrides

Returns:

  • nil on success
  • *Error on failure

Error Types

Error

type Error struct {
    Fields    []FieldError
    Truncated bool
}

Main validation error type containing multiple field errors.

Fields:

  • Fields - Slice of field-level errors
  • Truncated - True if errors were truncated due to maxErrors limit

Methods:

func (e Error) Error() string
func (e Error) Unwrap() error                    // Returns ErrValidation
func (e Error) HTTPStatus() int                  // Returns 422
func (e Error) Code() string                     // Returns "validation_error"
func (e Error) Details() any                     // Returns Fields
func (e *Error) Add(path, code, message string, meta map[string]any)
func (e *Error) AddError(err error)
func (e Error) HasErrors() bool
func (e Error) HasCode(code string) bool
func (e Error) Has(path string) bool
func (e Error) GetField(path string) *FieldError
func (e *Error) Sort()

Example:

var verr *validation.Error
if errors.As(err, &verr) {
    fmt.Printf("Found %d errors\n", len(verr.Fields))
    
    if verr.Truncated {
        fmt.Println("(more errors exist)")
    }
    
    if verr.Has("email") {
        fmt.Println("Email field has an error")
    }
}

FieldError

type FieldError struct {
    Path    string
    Code    string
    Message string
    Meta    map[string]any
}

Individual field validation error.

Fields:

  • Path - JSON path to the field (e.g., "items.2.price")
  • Code - Stable error code (e.g., "tag.required", "schema.type")
  • Message - Human-readable error message
  • Meta - Additional metadata (tag, param, value, etc.)

Methods:

func (e FieldError) Error() string    // Returns "path: message"
func (e FieldError) Unwrap() error    // Returns ErrValidation
func (e FieldError) HTTPStatus() int  // Returns 422

Example:

for _, fieldErr := range verr.Fields {
    fmt.Printf("Field: %s\n", fieldErr.Path)
    fmt.Printf("Code: %s\n", fieldErr.Code)
    fmt.Printf("Message: %s\n", fieldErr.Message)
    
    if tag, ok := fieldErr.Meta["tag"].(string); ok {
        fmt.Printf("Tag: %s\n", tag)
    }
}

PresenceMap Type

type PresenceMap map[string]bool

Tracks which fields are present in a request body. Keys are JSON field paths.

Methods:

func (pm PresenceMap) Has(path string) bool
func (pm PresenceMap) HasPrefix(prefix string) bool
func (pm PresenceMap) LeafPaths() []string

Example:

presence := PresenceMap{
    "email": true,
    "address": true,
    "address.city": true,
}

if presence.Has("email") {
    // Email was provided
}

if presence.HasPrefix("address") {
    // At least one address field was provided
}

leaves := presence.LeafPaths()
// Returns: ["email", "address.city"]
// (address is excluded as it has children)

Strategy Type

type Strategy int

const (
    StrategyAuto Strategy = iota
    StrategyTags
    StrategyJSONSchema
    StrategyInterface
)

Defines the validation approach to use.

Constants:

  • StrategyAuto - Automatically select best strategy (default)
  • StrategyTags - Use struct tag validation
  • StrategyJSONSchema - Use JSON Schema validation
  • StrategyInterface - Use interface methods (Validate() / ValidateContext())

Example:

err := validation.Validate(ctx, &user,
    validation.WithStrategy(validation.StrategyTags),
)

Sentinel Errors

var (
    ErrValidation                 = errors.New("validation")
    ErrCannotValidateNilValue     = errors.New("cannot validate nil value")
    ErrCannotValidateInvalidValue = errors.New("cannot validate invalid value")
    ErrUnknownValidationStrategy  = errors.New("unknown validation strategy")
    ErrValidationFailed           = errors.New("validation failed")
    ErrInvalidType                = errors.New("invalid type")
)

Sentinel errors for error checking with errors.Is.

Example:

if errors.Is(err, validation.ErrValidation) {
    // This is a validation error
}

Type Definitions

Option

type Option func(*config)

Functional option for configuring validation. See Options for all available options.

Redactor

type Redactor func(path string) bool

Function that determines if a field should be redacted in error messages. Returns true if the field at the given path should have its value hidden.

Example:

redactor := func(path string) bool {
    return strings.Contains(path, "password") ||
           strings.Contains(path, "token")
}

validator := validation.MustNew(
    validation.WithRedactor(redactor),
)

MessageFunc

type MessageFunc func(param string, kind reflect.Kind) string

Generates dynamic error messages for parameterized validation tags. Receives the tag parameter and field’s reflect.Kind.

Example:

minMessage := func(param string, kind reflect.Kind) string {
    if kind == reflect.String {
        return fmt.Sprintf("must be at least %s characters", param)
    }
    return fmt.Sprintf("must be at least %s", param)
}

validator := validation.MustNew(
    validation.WithMessageFunc("min", minMessage),
)

Constants

const (
    defaultMaxCachedSchemas = 1024
    maxRecursionDepth      = 100
)
  • defaultMaxCachedSchemas - Default JSON Schema cache size
  • maxRecursionDepth - Maximum nesting depth for ComputePresence

Thread Safety

All types and functions in the validation package are safe for concurrent use by multiple goroutines:

  • Validator instances are thread-safe
  • Package-level functions use a shared thread-safe default validator
  • PresenceMap is read-only after creation (safe for concurrent reads)

Performance

  • First validation of a type: ~500ns overhead for reflection
  • Subsequent validations: ~50ns overhead (cache lookup)
  • Schema compilation: Cached with LRU eviction
  • Path computation: Cached per type
  • Zero allocations: For cached types

Next Steps

2 - Options

Configuration options for validators

Complete reference for all configuration options (With* functions) available in the validation package.

Option Types

Options can be used in two ways:

  1. Validator Creation: Pass to New() or MustNew(). Applies to all validations.
  2. Per-Call: Pass to Validate() or ValidatePartial(). Applies to that call only.
// Validator creation options
validator := validation.MustNew(
    validation.WithMaxErrors(10),
    validation.WithRedactor(redactor),
)

// Per-call options (override validator config)
err := validator.Validate(ctx, &req,
    validation.WithMaxErrors(5), // Overrides the 10 from creation
    validation.WithStrategy(validation.StrategyTags),
)

Strategy Options

WithStrategy

func WithStrategy(strategy Strategy) Option

Sets the validation strategy to use.

Values:

  • StrategyAuto - Automatically select best strategy. This is the default.
  • StrategyTags - Use struct tags only.
  • StrategyJSONSchema - Use JSON Schema only.
  • StrategyInterface - Use interface methods only.

Example:

err := validation.Validate(ctx, &req,
    validation.WithStrategy(validation.StrategyTags),
)

WithRunAll

func WithRunAll(runAll bool) Option

Runs all applicable validation strategies and aggregates errors. By default, validation stops at the first successful strategy.

Example:

err := validation.Validate(ctx, &req,
    validation.WithRunAll(true),
)

WithRequireAny

func WithRequireAny(require bool) Option

When used with WithRunAll(true), succeeds if at least one strategy passes (OR logic).

Example:

// Pass if ANY strategy succeeds
err := validation.Validate(ctx, &req,
    validation.WithRunAll(true),
    validation.WithRequireAny(true),
)

Partial Validation Options

WithPartial

func WithPartial(partial bool) Option

Enables partial validation mode for PATCH requests. Validates only present fields and ignores “required” constraints for absent fields.

Example:

err := validation.Validate(ctx, &req,
    validation.WithPartial(true),
    validation.WithPresence(presenceMap),
)

WithPresence

func WithPresence(presence PresenceMap) Option

Sets the presence map for partial validation. Tracks which fields were provided in the request body.

Example:

presence, _ := validation.ComputePresence(rawJSON)
err := validation.Validate(ctx, &req,
    validation.WithPresence(presence),
    validation.WithPartial(true),
)

Error Limit Options

WithMaxErrors

func WithMaxErrors(maxErrors int) Option

Limits the number of errors returned. Set to 0 for unlimited errors (default).

Example:

// Return at most 5 errors
err := validation.Validate(ctx, &req,
    validation.WithMaxErrors(5),
)

var verr *validation.Error
if errors.As(err, &verr) {
    if verr.Truncated {
        fmt.Println("More errors exist")
    }
}

WithMaxFields

func WithMaxFields(maxFields int) Option

Sets the maximum number of fields to validate in partial mode. Prevents pathological inputs with extremely large presence maps. Set to 0 to use the default (10000).

Example:

validator := validation.MustNew(
    validation.WithMaxFields(5000),
)

Cache Options

WithMaxCachedSchemas

func WithMaxCachedSchemas(maxCachedSchemas int) Option

Sets the maximum number of JSON schemas to cache. Uses LRU eviction when limit is reached. Set to 0 to use the default (1024).

Example:

validator := validation.MustNew(
    validation.WithMaxCachedSchemas(2048),
)

Security Options

WithRedactor

func WithRedactor(redactor Redactor) Option

Sets a redactor function to hide sensitive values in error messages. The redactor returns true if the field at the given path should be redacted.

Example:

redactor := func(path string) bool {
    return strings.Contains(path, "password") ||
           strings.Contains(path, "token") ||
           strings.Contains(path, "secret")
}

validator := validation.MustNew(
    validation.WithRedactor(redactor),
)

WithDisallowUnknownFields

func WithDisallowUnknownFields(disallow bool) Option

Rejects JSON with unknown fields (typo detection). When enabled, causes strict JSON binding to reject requests with fields not defined in the struct.

Example:

err := validation.Validate(ctx, &req,
    validation.WithDisallowUnknownFields(true),
)

Context Options

WithContext

func WithContext(ctx context.Context) Option

Overrides the context used for validation. Useful when you need a different context than the one passed to Validate().

Note: In most cases, you should pass the context directly to Validate(). This option exists for advanced use cases.

Example:

err := validator.Validate(requestCtx, &req,
    validation.WithContext(backgroundCtx),
)

Custom Validation Options

WithCustomSchema

func WithCustomSchema(id, schema string) Option

Sets a custom JSON Schema for validation. This overrides any schema provided by the JSONSchemaProvider interface.

Example:

customSchema := `{
    "type": "object",
    "properties": {
        "email": {"type": "string", "format": "email"}
    }
}`

err := validation.Validate(ctx, &req,
    validation.WithCustomSchema("custom-user", customSchema),
)

WithCustomValidator

func WithCustomValidator(fn func(any) error) Option

Sets a custom validation function that runs before any other validation strategies.

Example:

err := validation.Validate(ctx, &req,
    validation.WithCustomValidator(func(v any) error {
        req := v.(*UserRequest)
        if req.Age < 18 {
            return errors.New("must be 18 or older")
        }
        return nil
    }),
)

WithCustomTag

func WithCustomTag(name string, fn validator.Func) Option

Registers a custom validation tag for use in struct tags. Custom tags are registered when the validator is created.

Example:

phoneValidator := func(fl validator.FieldLevel) bool {
    return phoneRegex.MatchString(fl.Field().String())
}

validator := validation.MustNew(
    validation.WithCustomTag("phone", phoneValidator),
)

type User struct {
    Phone string `validate:"phone"`
}

Error Message Options

WithMessages

func WithMessages(messages map[string]string) Option

Sets static error messages for validation tags. Messages override the default English messages for specified tags.

Example:

validator := validation.MustNew(
    validation.WithMessages(map[string]string{
        "required": "cannot be empty",
        "email":    "invalid email format",
        "min":      "value too small",
    }),
)

WithMessageFunc

func WithMessageFunc(tag string, fn MessageFunc) Option

Sets a dynamic message generator for a parameterized tag. Use for tags like “min”, “max”, “len” that include parameters.

Example:

minMessage := func(param string, kind reflect.Kind) string {
    if kind == reflect.String {
        return fmt.Sprintf("must be at least %s characters", param)
    }
    return fmt.Sprintf("must be at least %s", param)
}

validator := validation.MustNew(
    validation.WithMessageFunc("min", minMessage),
)

Field Name Options

WithFieldNameMapper

func WithFieldNameMapper(mapper func(string) string) Option

Sets a function to transform field names in error messages. Useful for localization or custom naming conventions.

Example:

validator := validation.MustNew(
    validation.WithFieldNameMapper(func(name string) string {
        // Convert snake_case to Title Case
        return strings.Title(strings.ReplaceAll(name, "_", " "))
    }),
)

Options Summary

Validator Creation Options

Options that should be set when creating a validator (affect all validations):

OptionPurpose
WithMaxErrorsLimit total errors returned
WithMaxFieldsLimit fields in partial validation
WithMaxCachedSchemasSchema cache size
WithRedactorRedact sensitive fields
WithCustomTagRegister custom validation tag
WithMessagesCustom error messages
WithMessageFuncDynamic error messages
WithFieldNameMapperTransform field names

Per-Call Options

Options commonly used per-call (override validator config):

OptionPurpose
WithStrategyChoose validation strategy
WithRunAllRun all strategies
WithRequireAnyOR logic with WithRunAll
WithPartialEnable partial validation
WithPresenceSet presence map
WithMaxErrorsOverride error limit
WithCustomValidatorAdd custom validator
WithCustomSchemaOverride JSON Schema
WithDisallowUnknownFieldsReject unknown fields
WithContextOverride context

Usage Patterns

Creating Configured Validator

validator := validation.MustNew(
    // Security
    validation.WithRedactor(sensitiveRedactor),
    validation.WithMaxErrors(20),
    validation.WithMaxFields(5000),
    
    // Custom validation
    validation.WithCustomTag("phone", phoneValidator),
    validation.WithCustomTag("username", usernameValidator),
    
    // Error messages
    validation.WithMessages(map[string]string{
        "required": "is required",
        "email":    "must be a valid email",
    }),
)

Per-Call Overrides

// Use tags strategy only
err := validator.Validate(ctx, &req,
    validation.WithStrategy(validation.StrategyTags),
    validation.WithMaxErrors(5),
)

// Partial validation
err := validator.Validate(ctx, &req,
    validation.WithPartial(true),
    validation.WithPresence(presence),
)

// Custom validation
err := validator.Validate(ctx, &req,
    validation.WithCustomValidator(complexBusinessLogic),
)

Next Steps

3 - Interfaces

Custom validation interfaces

Complete reference for validation interfaces that can be implemented for custom validation logic.

ValidatorInterface

type ValidatorInterface interface {
    Validate() error
}

Implement this interface for simple custom validation without context.

When to Use

  • Simple validation rules that don’t need external data
  • Business logic validation
  • Cross-field validation within the struct

Implementation

type User struct {
    Email string
    Name  string
}

func (u *User) Validate() error {
    if !strings.Contains(u.Email, "@") {
        return errors.New("email must contain @")
    }
    if len(u.Name) < 2 {
        return errors.New("name must be at least 2 characters")
    }
    return nil
}

Returning Structured Errors

Return *validation.Error for detailed field-level errors:

func (u *User) Validate() error {
    var verr validation.Error
    
    if !strings.Contains(u.Email, "@") {
        verr.Add("email", "format", "must contain @", nil)
    }
    
    if len(u.Name) < 2 {
        verr.Add("name", "length", "must be at least 2 characters", nil)
    }
    
    if verr.HasErrors() {
        return &verr
    }
    return nil
}

Pointer vs Value Receivers

Both are supported:

// Pointer receiver (can modify struct)
func (u *User) Validate() error {
    u.Email = strings.ToLower(u.Email) // Normalize
    return validateEmail(u.Email)
}

// Value receiver (read-only)
func (u User) Validate() error {
    return validateEmail(u.Email)
}

Use pointer receivers when you need to modify the struct during validation (normalization, etc.).

ValidatorWithContext

type ValidatorWithContext interface {
    ValidateContext(context.Context) error
}

Implement this interface for context-aware validation that needs access to request-scoped data or external services.

When to Use

  • Database lookups (uniqueness checks, existence validation)
  • Tenant-specific validation rules
  • Rate limiting or quota checks
  • External service calls
  • Request-scoped data access

Implementation

type User struct {
    Username string
    Email    string
    TenantID string
}

func (u *User) ValidateContext(ctx context.Context) error {
    // Get services from context
    db := ctx.Value("db").(*sql.DB)
    tenant := ctx.Value("tenant").(string)
    
    // Tenant validation
    if u.TenantID != tenant {
        return errors.New("user does not belong to this tenant")
    }
    
    // Database validation
    var exists bool
    err := db.QueryRowContext(ctx,
        "SELECT EXISTS(SELECT 1 FROM users WHERE username = $1)",
        u.Username,
    ).Scan(&exists)
    
    if err != nil {
        return fmt.Errorf("failed to check username: %w", err)
    }
    
    if exists {
        return errors.New("username already taken")
    }
    
    return nil
}

Context Values

Access data from context:

func (u *User) ValidateContext(ctx context.Context) error {
    // Database connection
    db := ctx.Value("db").(*sql.DB)
    
    // Current user/tenant
    currentUser := ctx.Value("user_id").(string)
    tenant := ctx.Value("tenant").(string)
    
    // Request metadata
    requestID := ctx.Value("request_id").(string)
    
    // Use in validation logic
    return validateWithContext(db, u, tenant)
}

Cancellation Support

Respect context cancellation for long-running validations:

func (u *User) ValidateContext(ctx context.Context) error {
    // Check cancellation before expensive operation
    select {
    case <-ctx.Done():
        return ctx.Err()
    default:
    }
    
    // Expensive validation
    return checkUsernameUniqueness(ctx, u.Username)
}

JSONSchemaProvider

type JSONSchemaProvider interface {
    JSONSchema() (id, schema string)
}

Implement this interface to provide a JSON Schema for validation.

When to Use

  • Portable validation rules (shared with frontend/documentation)
  • Complex validation logic without code
  • RFC-compliant validation
  • Schema versioning

Implementation

type Product struct {
    Name     string  `json:"name"`
    Price    float64 `json:"price"`
    Category string  `json:"category"`
}

func (p Product) JSONSchema() (id, schema string) {
    return "product-v1", `{
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
            "name": {
                "type": "string",
                "minLength": 1,
                "maxLength": 100
            },
            "price": {
                "type": "number",
                "minimum": 0,
                "exclusiveMinimum": true
            },
            "category": {
                "type": "string",
                "enum": ["electronics", "clothing", "books"]
            }
        },
        "required": ["name", "price", "category"],
        "additionalProperties": false
    }`
}

Schema ID

The ID is used for caching:

func (p Product) JSONSchema() (id, schema string) {
    return "product-v1", schemaString
    //     ^^^^^^^^^^^ Used as cache key
}

Use versioned IDs (e.g., "product-v1", "product-v2") to invalidate cache when schema changes.

Schema Formats

Supported formats:

  • email - Email address
  • uri / url - URL
  • hostname - DNS hostname
  • ipv4 / ipv6 - IP addresses
  • date - Date (YYYY-MM-DD)
  • date-time - RFC3339 date-time
  • uuid - UUID

Embedded Schemas

For complex schemas, consider embedding:

import _ "embed"

//go:embed user_schema.json
var userSchemaJSON string

func (u User) JSONSchema() (id, schema string) {
    return "user-v1", userSchemaJSON
}

Redactor

type Redactor func(path string) bool

Function that determines if a field should be redacted in error messages.

When to Use

  • Protecting passwords, tokens, secrets
  • Hiding credit card numbers, SSNs
  • Redacting PII (personally identifiable information)
  • Compliance requirements (GDPR, PCI-DSS)

Implementation

func sensitiveFieldRedactor(path string) bool {
    pathLower := strings.ToLower(path)
    
    // Password fields
    if strings.Contains(pathLower, "password") {
        return true
    }
    
    // Tokens and secrets
    if strings.Contains(pathLower, "token") ||
       strings.Contains(pathLower, "secret") ||
       strings.Contains(pathLower, "key") {
        return true
    }
    
    // Payment information
    if strings.Contains(pathLower, "card") ||
       strings.Contains(pathLower, "cvv") ||
       strings.Contains(pathLower, "credit") {
        return true
    }
    
    return false
}

validator := validation.MustNew(
    validation.WithRedactor(sensitiveFieldRedactor),
)

Path-Based Redaction

Redact specific paths:

func pathRedactor(path string) bool {
    redactedPaths := map[string]bool{
        "user.password":          true,
        "payment.card_number":    true,
        "payment.cvv":            true,
        "auth.refresh_token":     true,
    }
    return redactedPaths[path]
}

Nested Field Redaction

func nestedRedactor(path string) bool {
    // Redact all fields under payment.*
    if strings.HasPrefix(path, "payment.") {
        return true
    }
    
    // Redact specific nested field
    if strings.HasPrefix(path, "user.credentials.") {
        return true
    }
    
    return false
}

Interface Priority

When multiple interfaces are implemented, they have different priorities:

Priority Order:

  1. ValidatorWithContext / ValidatorInterface (highest)
  2. Struct tags (validate:"...")
  3. JSONSchemaProvider (lowest)
type User struct {
    Email string `validate:"required,email"` // Priority 2
}

func (u User) JSONSchema() (id, schema string) {
    // Priority 3 (lowest)
    return "user-v1", `{...}`
}

func (u *User) Validate() error {
    // Priority 1 (highest) - this runs instead of tags/schema
    return customValidation(u.Email)
}

Override priority with explicit strategy:

// Skip Validate() method, use tags
err := validation.Validate(ctx, &user,
    validation.WithStrategy(validation.StrategyTags),
)

Combining Interfaces

Run all strategies with WithRunAll:

type User struct {
    Email string `validate:"required,email"` // Struct tags
}

func (u User) JSONSchema() (id, schema string) {
    // JSON Schema
    return "user-v1", `{...}`
}

func (u *User) Validate() error {
    // Interface method
    return businessLogic(u)
}

// Run all three strategies
err := validation.Validate(ctx, &user,
    validation.WithRunAll(true),
)

Best Practices

1. Choose the Right Interface

// Simple validation - ValidatorInterface
func (u *User) Validate() error {
    return validateEmail(u.Email)
}

// Needs external data - ValidatorWithContext
func (u *User) ValidateContext(ctx context.Context) error {
    db := ctx.Value("db").(*sql.DB)
    return checkUniqueness(ctx, db, u.Email)
}

2. Return Structured Errors

// Good
func (u *User) Validate() error {
    var verr validation.Error
    verr.Add("email", "invalid", "must be valid email", nil)
    return &verr
}

// Bad
func (u *User) Validate() error {
    return errors.New("email invalid")
}

3. Use Context Safely

func (u *User) ValidateContext(ctx context.Context) error {
    db, ok := ctx.Value("db").(*sql.DB)
    if !ok {
        return errors.New("database not available in context")
    }
    return validateWithDB(ctx, db, u)
}

4. Document Custom Validation

// ValidateContext validates the user against business rules:
// - Username must be unique within tenant
// - Email domain must be allowed for tenant
// - User must not exceed account limits
func (u *User) ValidateContext(ctx context.Context) error {
    // Implementation
}

Testing

Testing ValidatorInterface

func TestUserValidation(t *testing.T) {
    tests := []struct {
        name    string
        user    User
        wantErr bool
    }{
        {"valid", User{Email: "test@example.com"}, false},
        {"invalid", User{Email: "invalid"}, true},
    }
    
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            err := tt.user.Validate()
            if (err != nil) != tt.wantErr {
                t.Errorf("Validate() error = %v, wantErr %v", err, tt.wantErr)
            }
        })
    }
}

Testing ValidatorWithContext

func TestUserValidationWithContext(t *testing.T) {
    ctx := context.Background()
    ctx = context.WithValue(ctx, "db", mockDB)
    ctx = context.WithValue(ctx, "tenant", "test-tenant")
    
    user := User{Username: "testuser"}
    err := user.ValidateContext(ctx)
    
    if err != nil {
        t.Errorf("ValidateContext() error = %v", err)
    }
}

Next Steps

4 - Validation Strategies

Strategy selection and priority

Complete reference for validation strategies, automatic selection, and priority order.

Overview

The validation package supports three validation strategies that can be used individually or combined:

  1. Interface Methods - Validate() / ValidateContext()
  2. Struct Tags - validate:"..." tags
  3. JSON Schema - JSONSchemaProvider interface

Strategy Types

StrategyAuto

const StrategyAuto Strategy = iota

Automatically selects the best strategy based on the type (default behavior).

Priority Order:

  1. Interface methods (highest priority)
  2. Struct tags
  3. JSON Schema (lowest priority)

Example:

// Uses automatic strategy selection
err := validation.Validate(ctx, &user)

StrategyTags

const StrategyTags Strategy = ...

Uses struct tag validation with go-playground/validator.

Requirements:

  • Struct type
  • Fields with validate tags

Example:

type User struct {
    Email string `validate:"required,email"`
}

err := validation.Validate(ctx, &user,
    validation.WithStrategy(validation.StrategyTags),
)

StrategyJSONSchema

const StrategyJSONSchema Strategy = ...

Uses JSON Schema validation (RFC-compliant).

Requirements:

  • Type implements JSONSchemaProvider interface, OR
  • Custom schema provided with WithCustomSchema

Example:

func (u User) JSONSchema() (id, schema string) {
    return "user-v1", `{...}`
}

err := validation.Validate(ctx, &user,
    validation.WithStrategy(validation.StrategyJSONSchema),
)

StrategyInterface

const StrategyInterface Strategy = ...

Uses custom interface methods (Validate() or ValidateContext()).

Requirements:

  • Type implements ValidatorInterface or ValidatorWithContext

Example:

func (u *User) Validate() error {
    return customValidation(u)
}

err := validation.Validate(ctx, &user,
    validation.WithStrategy(validation.StrategyInterface),
)

Automatic Strategy Selection

Selection Process

When StrategyAuto is used (default), the validator checks strategies in priority order:

graph TD
    Start[Start Validation] --> CheckInterface{Implements<br/>ValidatorInterface or<br/>ValidatorWithContext?}
    
    CheckInterface -->|Yes| UseInterface[Use Interface Strategy]:::success
    CheckInterface -->|No| CheckTags{Has validate<br/>tags?}
    
    CheckTags -->|Yes| UseTags[Use Tags Strategy]:::warning
    CheckTags -->|No| CheckSchema{Implements<br/>JSONSchemaProvider?}
    
    CheckSchema -->|Yes| UseSchema[Use JSON Schema Strategy]:::info
    CheckSchema -->|No| DefaultTags[Default to Tags]
    
    UseInterface --> Done[Validate]
    UseTags --> Done
    UseSchema --> Done
    DefaultTags --> Done
    
    classDef default fill:#F8FAF9,stroke:#1E6F5C,color:#1F2A27
    classDef info fill:#D1ECF1,stroke:#17A2B8,color:#1F2A27
    classDef success fill:#D4EDDA,stroke:#28A745,color:#1F2A27
    classDef warning fill:#FFF3CD,stroke:#FFC107,color:#1F2A27

Applicability Checks

A strategy is considered “applicable” if:

Interface Strategy:

  • Type implements ValidatorInterface or ValidatorWithContext
  • Checks both value and pointer receivers

Tags Strategy:

  • Type is a struct
  • At least one field has a validate tag

JSON Schema Strategy:

  • Type implements JSONSchemaProvider, OR
  • Custom schema provided with WithCustomSchema

Priority Examples

// Example 1: Only interface method
type User struct {
    Email string
}

func (u *User) Validate() error {
    return validateEmail(u.Email)
}

// Uses: StrategyInterface (highest priority)
validation.Validate(ctx, &user)
// Example 2: Both interface and tags
type User struct {
    Email string `validate:"required,email"`
}

func (u *User) Validate() error {
    return customLogic(u.Email)
}

// Uses: StrategyInterface (interface has priority over tags)
validation.Validate(ctx, &user)
// Example 3: All three strategies
type User struct {
    Email string `validate:"required,email"`
}

func (u User) JSONSchema() (id, schema string) {
    return "user-v1", `{...}`
}

func (u *User) Validate() error {
    return customLogic(u.Email)
}

// Uses: StrategyInterface (highest priority)
validation.Validate(ctx, &user)

Explicit Strategy Selection

Override automatic selection with WithStrategy:

type User struct {
    Email string `validate:"required,email"` // Has tags
}

func (u *User) Validate() error {
    return customLogic(u.Email) // Has interface method
}

// Force use of tags (skip interface method)
err := validation.Validate(ctx, &user,
    validation.WithStrategy(validation.StrategyTags),
)

Running Multiple Strategies

WithRunAll

Run all applicable strategies and aggregate errors:

type User struct {
    Email string `validate:"required,email"` // Tags
}

func (u User) JSONSchema() (id, schema string) {
    return "user-v1", `{...}` // JSON Schema
}

func (u *User) Validate() error {
    return customLogic(u.Email) // Interface
}

// Run all three strategies
err := validation.Validate(ctx, &user,
    validation.WithRunAll(true),
)

// All errors from all strategies are collected
var verr *validation.Error
if errors.As(err, &verr) {
    // verr.Fields contains errors from all strategies
}

WithRequireAny

With WithRunAll, succeed if any one strategy passes (OR logic):

// Pass if ANY strategy succeeds
err := validation.Validate(ctx, &user,
    validation.WithRunAll(true),
    validation.WithRequireAny(true),
)

Use Cases:

  • Multiple validation approaches, any one is sufficient
  • Fallback validation strategies
  • Gradual migration between strategies

Strategy Comparison

StrategyAdvantagesDisadvantagesBest For
InterfaceMost flexible, full programmatic controlMore code, not declarativeComplex business logic, database checks
TagsConcise, declarative, well-documentedLimited to supported tagsStandard validation, simple rules
JSON SchemaPortable, language-independentVerbose, learning curveShared validation with frontend

Strategy Patterns

Pattern 1: Tags for Simple, Interface for Complex

type User struct {
    Email    string `validate:"required,email"`
    Username string `validate:"required,min=3,max=20"`
    Age      int    `validate:"required,min=18"`
}

func (u *User) ValidateContext(ctx context.Context) error {
    // Complex validation (database checks, etc.)
    db := ctx.Value("db").(*sql.DB)
    return checkUsernameUnique(ctx, db, u.Username)
}

// Tags validate format, interface validates business rules
validation.Validate(ctx, &user)

Pattern 2: Schema for API, Interface for Internal

func (u User) JSONSchema() (id, schema string) {
    // For external API documentation/validation
    return "user-v1", apiSchema
}

func (u *User) ValidateContext(ctx context.Context) error {
    // Internal business rules
    return validateInternal(ctx, u)
}

// External API: use schema
validation.Validate(ctx, &user,
    validation.WithStrategy(validation.StrategyJSONSchema),
)

// Internal: use interface
validation.Validate(ctx, &user,
    validation.WithStrategy(validation.StrategyInterface),
)

Pattern 3: Progressive Enhancement

// Start with tags
type User struct {
    Email string `validate:"required,email"`
}

// Add interface for complex validation later
func (u *User) ValidateContext(ctx context.Context) error {
    // Complex validation added over time
    return additionalValidation(ctx, u)
}

// Automatically uses interface (higher priority)
validation.Validate(ctx, &user)

Performance Considerations

Strategy Performance

Fastest to Slowest:

  1. Tags - Cached reflection, zero allocation after first use
  2. Interface - Direct method call, user code performance
  3. JSON Schema - Schema compilation (cached), RFC validation

Optimization Tips

// Fast: Use tags for simple validation
type User struct {
    Email string `validate:"required,email"`
}

// Slower: JSON Schema (first time, then cached)
func (u User) JSONSchema() (id, schema string) {
    return "user-v1", complexSchema
}

// Variable: Depends on your implementation
func (u *User) ValidateContext(ctx context.Context) error {
    // Keep this fast - runs every time
    return quickValidation(u)
}

Caching

  • Tags: Struct reflection cached per type
  • JSON Schema: Schemas cached by ID (LRU eviction)
  • Interface: No caching (direct method call)

Error Aggregation

When running multiple strategies:

err := validation.Validate(ctx, &user,
    validation.WithRunAll(true),
)

var verr *validation.Error
if errors.As(err, &verr) {
    // Errors are aggregated from all strategies
    for _, fieldErr := range verr.Fields {
        fmt.Printf("%s: %s (from %s strategy)\n",
            fieldErr.Path,
            fieldErr.Message,
            inferStrategy(fieldErr.Code), // tag.*, schema.*, etc.
        )
    }
    
    // Sort for consistent output
    verr.Sort()
}

Error codes indicate strategy:

  • tag.* - From struct tags
  • schema.* - From JSON Schema
  • Custom codes - From interface methods

Best Practices

1. Use Automatic Selection

// Good - let validator choose
validation.Validate(ctx, &user)

// Only override when necessary
validation.Validate(ctx, &user,
    validation.WithStrategy(validation.StrategyTags),
)

2. Single Strategy Per Type

// Good - clear which strategy is used
type User struct {
    Email string `validate:"required,email"`
}

// Confusing - multiple strategies compete
type User struct {
    Email string `validate:"required,email"`
}

func (u User) JSONSchema() (id, schema string) { ... }
func (u *User) Validate() error { ... }

3. Document Strategy Choice

// User validation uses struct tags for simplicity and performance.
// Email format and length are validated declaratively.
type User struct {
    Email string `validate:"required,email,max=255"`
}

4. Use WithRunAll Sparingly

// Most cases: automatic selection is sufficient
validation.Validate(ctx, &user)

// Only when you need to validate with multiple strategies
validation.Validate(ctx, &user,
    validation.WithRunAll(true),
)

Next Steps

5 - Troubleshooting

Common issues and solutions

Common issues, solutions, and debugging tips for the validation package.

Validation Not Running

Issue: Validation passes when it should fail

Symptom:

type User struct {
    Email string `validate:"required,email"`
}

user := User{Email: ""} // Should fail
err := validation.Validate(ctx, &user) // err is nil (unexpected)

Possible Causes:

  1. Struct tags not being checked

Check if a higher-priority strategy is being used:

func (u *User) Validate() error {
    return nil // This runs instead of tags
}

Solution: Remove interface method or use WithStrategy:

err := validation.Validate(ctx, &user,
    validation.WithStrategy(validation.StrategyTags),
)
  1. Wrong tag name
// Wrong
Email string `validation:"required"` // Should be "validate"

// Correct
Email string `validate:"required"`
  1. Validating value instead of pointer
// May not work with pointer receivers
user := User{} // value
user.Validate() // Method might not be found

// Use pointer
user := &User{} // pointer
validation.Validate(ctx, user)

Partial Validation Issues

Issue: Required fields failing in PATCH requests

Symptom:

type UpdateUser struct {
    Email string `validate:"required,email"`
}

// PATCH with only age
err := validation.ValidatePartial(ctx, &req, presence)
// Error: email is required (but it wasn't provided)

Solution: Use omitempty instead of required for PATCH:

type UpdateUser struct {
    Email string `validate:"omitempty,email"` // Not "required"
}

Issue: Presence map not being respected

Symptom:

presence, _ := validation.ComputePresence(rawJSON)
err := validation.Validate(ctx, &req, // Missing WithPresence!
    validation.WithPartial(true),
)

Solution: Always pass presence map:

err := validation.Validate(ctx, &req,
    validation.WithPartial(true),
    validation.WithPresence(presence), // Add this
)

Custom Validation Issues

Issue: Custom tag not working

Symptom:

validator := validation.MustNew(
    validation.WithCustomTag("phone", phoneValidator),
)

type User struct {
    Phone string `validate:"phone"` // Not recognized
}

Possible Causes:

  1. Tag registered on wrong validator
// Registered on custom validator
validator := validation.MustNew(
    validation.WithCustomTag("phone", phoneValidator),
)

// But using package-level function (different validator)
validation.Validate(ctx, &user) // Doesn't have custom tag

Solution: Use the same validator:

validator.Validate(ctx, &user) // Use custom validator
  1. Tag function signature wrong
// Wrong
func phoneValidator(val string) bool { ... }

// Correct
func phoneValidator(fl validator.FieldLevel) bool { ... }

Issue: ValidateContext not being called

Symptom:

func (u *User) ValidateContext(ctx context.Context) error {
    fmt.Println("Never prints")
    return nil
}

Possible Causes:

  1. Wrong receiver type
// Method defined on value
func (u User) ValidateContext(ctx context.Context) error { ... }

// But validating pointer
user := &User{}
validation.Validate(ctx, user) // Method not found

Solution: Use pointer receiver:

func (u *User) ValidateContext(ctx context.Context) error { ... }
  1. Struct tags have priority

If auto-selection chooses tags, interface method isn’t called.

Solution: Explicitly use interface strategy:

validation.Validate(ctx, &user,
    validation.WithStrategy(validation.StrategyInterface),
)

Error Handling Issues

Issue: Can’t access field errors

Symptom:

err := validation.Validate(ctx, &user)
// How do I get field-level errors?

Solution: Use errors.As:

var verr *validation.Error
if errors.As(err, &verr) {
    for _, fieldErr := range verr.Fields {
        fmt.Printf("%s: %s\n", fieldErr.Path, fieldErr.Message)
    }
}

Issue: Sensitive data visible in errors

Symptom:

// Error message contains password value
email: invalid email (value: "password123")

Solution: Use redactor:

validator := validation.MustNew(
    validation.WithRedactor(func(path string) bool {
        return strings.Contains(path, "password")
    }),
)

Performance Issues

Issue: Validation is slow

Possible Causes:

  1. Creating validator on every request
// Bad - creates validator every time
func Handler(w http.ResponseWriter, r *http.Request) {
    validator := validation.MustNew(...) // Slow
    validator.Validate(ctx, &req)
}

Solution: Create once, reuse:

var validator = validation.MustNew(...)

func Handler(w http.ResponseWriter, r *http.Request) {
    validator.Validate(ctx, &req) // Fast
}
  1. JSON Schema not cached
func (u User) JSONSchema() (id, schema string) {
    return "", `{...}` // Empty ID = no caching
}

Solution: Use stable ID:

func (u User) JSONSchema() (id, schema string) {
    return "user-v1", `{...}` // Cached
}
  1. Expensive ValidateContext
func (u *User) ValidateContext(ctx context.Context) error {
    // Expensive operation on every validation
    return checkWithExternalAPI(u.Email)
}

Solution: Optimize or cache:

func (u *User) ValidateContext(ctx context.Context) error {
    // Fast checks first
    if !basicValidation(u.Email) {
        return errors.New("invalid format")
    }
    
    // Expensive check last
    return checkWithExternalAPI(u.Email)
}

JSON Schema Issues

Issue: Schema validation not working

Symptom:

func (u User) JSONSchema() (id, schema string) {
    return "user-v1", `{...}`
}

// But validation doesn't use schema

Possible Causes:

  1. Higher priority strategy exists
type User struct {
    Email string `validate:"email"` // Tags have higher priority
}

func (u User) JSONSchema() (id, schema string) {
    return "user-v1", `{...}` // Not used
}

Solution: Use explicit strategy:

validation.Validate(ctx, &user,
    validation.WithStrategy(validation.StrategyJSONSchema),
)
  1. Invalid JSON Schema
func (u User) JSONSchema() (id, schema string) {
    return "user-v1", `{ invalid json }` // Parse error
}

Solution: Validate schema syntax:

// Use online validator: https://www.jsonschemavalidator.net/

Context Issues

Issue: Context values not available

Symptom:

func (u *User) ValidateContext(ctx context.Context) error {
    db := ctx.Value("db") // db is nil
    // ...
}

Solution: Ensure values are in context:

ctx = context.WithValue(ctx, "db", db)
err := validation.Validate(ctx, &user)

Issue: Wrong context being used

Symptom:

err := validation.Validate(ctx1, &user,
    validation.WithContext(ctx2), // Overrides ctx1
)

Solution: Don’t use WithContext unless necessary:

// Just pass the right context
err := validation.Validate(correctCtx, &user)

Module and Import Issues

Issue: Cannot find module

go: finding module for package rivaas.dev/validation

Solution:

go mod tidy
go get rivaas.dev/validation

Issue: Version conflicts

require rivaas.dev/validation v1.0.0
// +incompatible

Solution: Update to compatible version:

go get rivaas.dev/validation@latest
go mod tidy

Common Error Messages

“cannot validate nil value”

Cause: Passing nil to Validate:

var user *User
validation.Validate(ctx, user) // Error: cannot validate nil value

Solution: Ensure value is not nil:

user := &User{Email: "test@example.com"}
validation.Validate(ctx, user)

“cannot validate invalid value”

Cause: Passing invalid reflect.Value:

var v interface{}
validation.Validate(ctx, v) // Error: cannot validate invalid value

Solution: Pass actual struct:

user := &User{}
validation.Validate(ctx, user)

“unknown validation strategy”

Cause: Invalid strategy value:

validation.Validate(ctx, &user,
    validation.WithStrategy(999), // Invalid
)

Solution: Use valid strategy constants:

validation.Validate(ctx, &user,
    validation.WithStrategy(validation.StrategyTags),
)

Debugging Tips

1. Check which strategy is being used

// Temporarily force each strategy to see which works
strategies := []validation.Strategy{
    validation.StrategyInterface,
    validation.StrategyTags,
    validation.StrategyJSONSchema,
}

for _, strategy := range strategies {
    err := validation.Validate(ctx, &user,
        validation.WithStrategy(strategy),
    )
    fmt.Printf("%v: %v\n", strategy, err)
}

2. Enable all error reporting

err := validation.Validate(ctx, &user,
    validation.WithMaxErrors(0), // Unlimited
)

3. Check struct tags

import "reflect"

t := reflect.TypeOf(User{})
for i := 0; i < t.NumField(); i++ {
    field := t.Field(i)
    fmt.Printf("%s: %s\n", field.Name, field.Tag.Get("validate"))
}

4. Test interface implementation

var _ validation.ValidatorInterface = (*User)(nil) // Compile-time check
var _ validation.ValidatorWithContext = (*User)(nil)
var _ validation.JSONSchemaProvider = (*User)(nil)

Getting Help

If you’re still stuck:

  1. Check documentation: User Guide
  2. Review examples: Examples
  3. Check pkg.go.dev: API Documentation
  4. GitHub Issues: Report a bug
  5. Discussions: Ask a question

Next Steps