validation

package module
v0.5.4 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Feb 26, 2026 License: Apache-2.0 Imports: 14 Imported by: 1

README

Validation

Go Reference Go Report Card Coverage Go Version License

Flexible, multi-strategy validation for Go structs with support for struct tags, JSON Schema, and custom interfaces.

📚 Full Documentation: https://rivaas.dev/docs/guides/validation/

Documentation

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

Installation

go get rivaas.dev/validation

Requires Go 1.25+

Quick Start

import "rivaas.dev/validation"

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

user := User{Email: "invalid", Age: 15}
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)
        }
    }
}

Validation Strategies

1. Struct Tags
type User struct {
    Email string `validate:"required,email"`
    Age   int    `validate:"min=18,max=120"`
}
2. JSON Schema
func (u User) JSONSchema() (id, schema string) {
    return "user-v1", `{
        "type": "object",
        "properties": {
            "email": {"type": "string", "format": "email"},
            "age": {"type": "integer", "minimum": 18}
        },
        "required": ["email"]
    }`
}
3. Custom Interfaces
func (u *User) Validate() error {
    if !strings.Contains(u.Email, "@") {
        return errors.New("email must contain @")
    }
    return nil
}

Learn More

Contributing

Contributions are welcome! Please see the main repository for contribution guidelines.

License

Apache License 2.0 - see LICENSE for details.


Part of the Rivaas web framework ecosystem.

Documentation

Overview

Package validation provides flexible, multi-strategy validation for Go structs.

Getting Started

The simplest way to use this package is with the package-level Validate function:

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

user := User{Email: "invalid", Age: 15}
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)
		}
	}
}

For more control, create a Validator instance with New or MustNew:

validator := validation.MustNew(
	validation.WithRedactor(sensitiveFieldRedactor),
	validation.WithMaxErrors(10),
	validation.WithCustomTag("phone", phoneValidator),
)

if err := validator.Validate(ctx, &user); err != nil {
	// Handle validation errors
}

Validation Strategies

The package supports three validation strategies:

  1. Struct Tags - Using go-playground/validator tags (e.g., `validate:"required,email"`)
  2. JSON Schema - RFC-compliant JSON Schema validation via JSONSchemaProvider interface
  3. Custom Interfaces - Implement ValidatorInterface or ValidatorWithContext for custom validation logic

The package automatically selects the best strategy based on the value type, or you can explicitly choose a strategy using WithStrategy.

Partial Validation

For PATCH requests where only some fields are provided, use ValidatePartial:

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

Custom Validation Interface

Implement ValidatorInterface for custom validation logic:

type User struct {
	Email string
}

func (u *User) Validate() error {
	if !strings.Contains(u.Email, "@") {
		return errors.New("email must contain @")
	}
	return nil
}

// validation.Validate will automatically call u.Validate()
err := validation.Validate(ctx, &user)

For context-aware validation, implement ValidatorWithContext:

func (u *User) ValidateContext(ctx context.Context) error {
	// Access request-scoped data from context
	tenant := ctx.Value("tenant").(string)
	// Apply tenant-specific validation rules
	return nil
}

Thread Safety

Validator instances are safe for concurrent use by multiple goroutines. The package-level functions use a default validator that is also thread-safe.

Security

The package includes protections against:

  • Stack overflow from deeply nested structures (max depth: 100)
  • Unbounded memory usage (configurable limits on errors and fields)
  • Sensitive data exposure (redaction support via WithRedactor)

Standalone Usage

This package can be used independently without the full Rivaas framework:

import "rivaas.dev/validation"

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

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	// ErrCannotValidateNilValue is returned when attempting to validate a nil value.
	ErrCannotValidateNilValue = errors.New("cannot validate nil value")

	// ErrCannotValidateInvalidValue is returned when the value is not valid for reflection.
	ErrCannotValidateInvalidValue = errors.New("cannot validate invalid value")

	// ErrUnknownValidationStrategy is returned when an unknown validation strategy is specified.
	ErrUnknownValidationStrategy = errors.New("unknown validation strategy")

	// ErrValidationFailed is a generic validation failure error.
	ErrValidationFailed = errors.New("validation failed")

	// ErrInvalidType is returned when a value has an unexpected type.
	ErrInvalidType = errors.New("invalid type")
)

Predefined validation errors.

View Source
var ErrValidation = errors.New("validation")

ErrValidation is a sentinel error for validation failures. Use errors.Is(err, ErrValidation) to check if an error is a validation error.

Functions

func ExtractRawJSONCtx

func ExtractRawJSONCtx(ctx context.Context) ([]byte, bool)

ExtractRawJSONCtx retrieves raw JSON bytes from context if present. It returns (nil, false) if the context does not contain raw JSON (i.e., InjectRawJSONCtx was not called).

func InjectRawJSONCtx

func InjectRawJSONCtx(ctx context.Context, raw []byte) context.Context

InjectRawJSONCtx injects raw JSON bytes into context for StrategyJSONSchema validation.

It is an internal API used by request binding frameworks. The raw bytes are used directly for schema validation instead of re-encoding the Go struct back to JSON.

Do not call it directly in application code unless you are implementing a custom binding framework.

Parameters:

  • ctx: The parent context
  • raw: Raw JSON bytes from the request body

func Validate

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

Validate validates a value using the default Validator. For customized validation, create a Validator with New or MustNew.

Validate returns nil if validation passes, or an *Error if validation fails. The error can be type-asserted to *Error for structured field errors.

Parameters:

  • ctx: Context passed to ValidatorWithContext implementations
  • v: The value to validate (typically a pointer to a struct)
  • opts: Optional per-call configuration (see Option)

Example:

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

With options:

if err := validation.Validate(ctx, &req,
    validation.WithStrategy(StrategyTags),
    validation.WithPartial(true),
    validation.WithPresence(presenceMap),
); err != nil {
    // Handle validation error
}
Example

ExampleValidate demonstrates basic validation with struct tags.

package main

import (
	"context"
	"errors"
	"fmt"

	"rivaas.dev/validation"
)

func main() {
	type User struct {
		Email string `json:"email" validate:"required,email"`
		Age   int    `json:"age" validate:"min=18"`
	}

	user := User{Email: "invalid", Age: 15}
	err := validation.Validate(context.Background(), &user)
	if err != nil {
		var verr *validation.Error
		if errors.As(err, &verr) {
			for _, fieldErr := range verr.Fields {
				if _, err = fmt.Printf("%s: %s\n", fieldErr.Path, fieldErr.Message); err != nil {
					panic(err)
				}
			}
		}
	}
}
Output:

age: must be at least 18
email: must be a valid email address
Example (WithOptions)

ExampleValidate_withOptions demonstrates validation with various options.

package main

import (
	"context"
	"errors"
	"fmt"

	"rivaas.dev/validation"
)

func main() {
	type User struct {
		Password string `json:"password" validate:"required,min=8"` //nolint:gosec // G117: example fixture, no real credentials
		Token    string `json:"token" validate:"required"`
	}

	user := User{
		Password: "short",
		Token:    "secret-token-12345",
	}

	// Use redactor to hide sensitive fields
	redactor := func(path string) bool {
		return path == "password" || path == "token"
	}

	err := validation.Validate(context.Background(), &user,
		validation.WithRedactor(redactor),
		validation.WithMaxErrors(5),
	)
	if err != nil {
		var verr *validation.Error
		if errors.As(err, &verr) {
			for _, fieldErr := range verr.Fields {
				if _, printErr := fmt.Printf("%s: %s (value: %v)\n",
					fieldErr.Path,
					fieldErr.Message,
					fieldErr.Meta["value"],
				); printErr != nil {
					panic(printErr)
				}
			}
		}
	}
}
Output:

password: must be at least 8 characters (value: ***REDACTED***)

func ValidatePartial

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

ValidatePartial validates only fields present in the PresenceMap using the default Validator. ValidatePartial is useful for PATCH requests where only provided fields should be validated. Use ComputePresence to create a PresenceMap from raw JSON.

Example

ExampleValidatePartial demonstrates partial validation for PATCH requests.

package main

import (
	"context"
	"fmt"

	"rivaas.dev/validation"
)

func main() {
	type User struct {
		Name  string `json:"name" validate:"required"`
		Email string `json:"email" validate:"required,email"`
		Age   int    `json:"age" validate:"min=18"`
	}

	// Simulate a PATCH request that only updates email
	rawJSON := []byte(`{"email": "[email protected]"}`)
	presence, err := validation.ComputePresence(rawJSON)
	if err != nil {
		panic(err)
	}

	user := User{Name: "Existing Name", Email: "[email protected]", Age: 25}
	err = validation.ValidatePartial(context.Background(), &user, presence)

	if err != nil {
		if _, printErr := fmt.Printf("Validation failed: %v\n", err); printErr != nil {
			panic(printErr)
		}
	} else {
		if _, printErr := fmt.Println("Validation passed"); printErr != nil {
			panic(printErr)
		}
	}
}
Output:

Validation passed

Types

type Error

type Error struct {
	Fields    []FieldError `json:"errors"`              // List of field errors
	Truncated bool         `json:"truncated,omitempty"` // True if errors were truncated due to maxErrors limit
}

Error represents validation errors for one or more fields. Error implements error and can be used with errors.Is/errors.As. It contains a slice of FieldError values, one for each field that failed validation.

Example:

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

func (*Error) Add

func (v *Error) Add(path, code, message string, meta map[string]any)

Add adds a new FieldError to the collection.

Example:

var err Error
err.Add("email", "tag.required", "is required", map[string]any{"tag": "required"})

func (*Error) AddError

func (v *Error) AddError(err error)

AddError adds an error to the collection, handling different error types. AddError accepts FieldError, Error, or generic errors and converts them appropriately.

Example:

var err Error
err.AddError(FieldError{Path: "email", Code: "tag.required", Message: "is required"})

func (Error) Code

func (v Error) Code() string

Code implements rivaas.dev/errors.ErrorCode.

func (Error) Details

func (v Error) Details() any

Details implements rivaas.dev/errors.ErrorDetails.

func (Error) Error

func (v Error) Error() string

Error returns a formatted error message.

func (Error) GetField

func (v Error) GetField(path string) *FieldError

GetField returns the first FieldError for a given path, or nil if not found.

Example:

fieldErr := err.GetField("email")
if fieldErr != nil {
    fmt.Println(fieldErr.Message)
}

func (Error) HTTPStatus

func (v Error) HTTPStatus() int

HTTPStatus implements rivaas.dev/errors.ErrorType.

func (Error) Has

func (v Error) Has(path string) bool

Has checks if a specific field path has an error.

Example:

if err.Has("email") {
    // Handle email field errors
}

func (Error) HasCode

func (v Error) HasCode(code string) bool

HasCode returns true if any error has the given code.

Example:

if err.HasCode("tag.required") {
    // Handle required field errors
}

func (Error) HasErrors

func (v Error) HasErrors() bool

HasErrors returns true if there are any errors.

func (*Error) Sort

func (v *Error) Sort()

Sort sorts errors by path, then by code. Sort modifies the error in place and is useful for consistent error presentation.

func (Error) Unwrap

func (v Error) Unwrap() error

Unwrap returns ErrValidation for errors.Is/errors.As compatibility.

type FieldError

type FieldError struct {
	Path    string         `json:"path"`           // JSON path (e.g., "items.2.price")
	Code    string         `json:"code"`           // Stable code (e.g., "tag.required", "schema.type")
	Message string         `json:"message"`        // Human-readable message
	Meta    map[string]any `json:"meta,omitempty"` // Additional metadata (tag, param, value, etc.)
}

FieldError represents a single validation error for a specific field. Multiple FieldError values are collected in an Error.

Example:

err := FieldError{
    Path:    "email",
    Code:    "tag.required",
    Message: "is required",
    Meta:    map[string]any{"tag": "required"},
}

func (FieldError) Error

func (e FieldError) Error() string

Error returns a formatted error message as "path: message" or just "message" if path is empty.

func (FieldError) HTTPStatus

func (e FieldError) HTTPStatus() int

HTTPStatus implements rivaas.dev/errors.ErrorType.

func (FieldError) Unwrap

func (e FieldError) Unwrap() error

Unwrap returns ErrValidation for errors.Is/errors.As compatibility.

type JSONSchemaProvider

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

JSONSchemaProvider interface for types that provide their own JSON Schema. When implemented by a struct, it causes the returned schema to be used for validation.

Example:

type User struct {
    Email string `json:"email"`
}

func (u *User) JSONSchema() (id string, schema string) {
    return "user-v1", `{
        "type": "object",
        "properties": {
            "email": {"type": "string", "format": "email"}
        },
        "required": ["email"]
    }`
}

type MessageFunc added in v0.5.0

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

MessageFunc generates a dynamic error message for parameterized validation tags. Use WithMessageFunc to configure messages for tags like "min", "max", "len", "oneof" that include parameters.

The function receives the tag parameter (e.g., "3" for `min=3`) and the field's reflect.Kind to enable type-aware messages (e.g., "characters" for strings vs plain numbers for integers).

type Option

type Option func(*config)

Option is a functional option for configuring validation. Options can be passed to New, MustNew, Validate, or Validator.Validate.

func WithContext

func WithContext(ctx context.Context) Option

WithContext overrides the context used for validation. This is useful when you need a different context than the one passed to Validate().

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

Example:

validator.Validate(requestCtx, &req, WithContext(backgroundCtx))

func WithCustomSchema

func WithCustomSchema(id, schema string) Option

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

Example:

schema := `{"type": "object", "properties": {"email": {"type": "string", "format": "email"}}}`
validator.Validate(ctx, &req, WithCustomSchema("user-schema", schema))

func WithCustomTag added in v0.2.0

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

WithCustomTag registers a custom validation tag for use in struct tags. Custom tags are registered when the Validator is created.

Example:

validator := validation.MustNew(
    validation.WithCustomTag("phone", func(fl validator.FieldLevel) bool {
        return phoneRegex.MatchString(fl.Field().String())
    }),
)

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

func WithCustomValidator

func WithCustomValidator(fn func(any) error) Option

WithCustomValidator sets a custom validation function. It calls the function before any other validation strategies.

Example:

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

func WithDisallowUnknownFields

func WithDisallowUnknownFields(disallow bool) Option

WithDisallowUnknownFields rejects JSON with unknown fields (typo detection). When enabled, it causes BindJSONStrict to reject requests with fields not defined in the struct.

Example:

BindAndValidateStrict(&req, WithDisallowUnknownFields(true))

func WithFieldNameMapper

func WithFieldNameMapper(mapper func(string) string) Option

WithFieldNameMapper sets a function to transform field names in error messages. It is useful for localization or custom naming conventions.

Example:

validator.Validate(ctx, &req, WithFieldNameMapper(func(name string) string {
    return strings.ReplaceAll(name, "_", " ")
}))

func WithMaxCachedSchemas

func WithMaxCachedSchemas(maxCachedSchemas int) Option

WithMaxCachedSchemas sets the maximum number of JSON schemas to cache. Set to 0 to use the default (1024).

Example:

validation.MustNew(validation.WithMaxCachedSchemas(2048))

func WithMaxErrors

func WithMaxErrors(maxErrors int) Option

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

Example:

validator.Validate(ctx, &req, WithMaxErrors(10))

func WithMaxFields

func WithMaxFields(maxFields int) Option

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

Example:

validator.Validate(ctx, &req, WithMaxFields(5000), WithPartial(true))

func WithMessageFunc added in v0.5.0

func WithMessageFunc(tag string, fn MessageFunc) Option

WithMessageFunc sets a dynamic message generator for a parameterized tag. Use for tags like "min", "max", "len", "oneof" that include parameters.

Example:

validator := validation.MustNew(
    validation.WithMessageFunc("min", func(param string, kind reflect.Kind) string {
        if kind == reflect.String {
            return fmt.Sprintf("too short (min %s chars)", param)
        }
        return fmt.Sprintf("too small (min %s)", param)
    }),
)

func WithMessages added in v0.5.0

func WithMessages(messages map[string]string) Option

WithMessages sets static error messages for validation tags. Messages override the default English messages for specified tags. Unspecified tags continue to use defaults.

Example:

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

func WithPartial

func WithPartial(partial bool) Option

WithPartial enables partial update validation mode (for PATCH requests). It validates only present fields and ignores "required" constraints for absent fields.

Example:

validator.Validate(ctx, &req, WithPartial(true), WithPresence(presenceMap))

func WithPresence

func WithPresence(presence PresenceMap) Option

WithPresence sets the presence map for partial validation. The PresenceMap tracks which fields were provided in the request body. Use ComputePresence to create a PresenceMap from raw JSON.

Example:

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

func WithRedactor

func WithRedactor(redactor Redactor) Option

WithRedactor 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:

validator.Validate(ctx, &req, WithRedactor(func(path string) bool {
    return strings.Contains(path, "password") || strings.Contains(path, "token")
}))

func WithRequireAny

func WithRequireAny(require bool) Option

WithRequireAny requires at least one validation strategy to pass when using WithRunAll. When enabled with WithRunAll(true), it causes validation to succeed if at least one strategy produces no errors, even if other strategies fail.

It is useful when you have multiple validation strategies and want to accept the value if it passes any one of them (OR logic).

Example:

// Validate with all strategies, succeed if any one passes
err := validator.Validate(ctx, &req,
    WithRunAll(true),
    WithRequireAny(true),
)

func WithRunAll

func WithRunAll(runAll bool) Option

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

Example:

validator.Validate(ctx, &req, WithRunAll(true))

func WithStrategy

func WithStrategy(strategy Strategy) Option

WithStrategy sets the validation strategy.

Example:

validator.Validate(ctx, &req, WithStrategy(StrategyTags))

type PresenceMap

type PresenceMap map[string]bool

PresenceMap tracks which fields are present in the request body. Keys are normalized dot paths (e.g., "items.2.price"), values are always true.

PresenceMap is used for partial update validation (PATCH requests), where only present fields should be validated, while absent fields should be ignored even if they have "required" constraints.

Use ComputePresence to create a PresenceMap from raw JSON, and WithPresence to pass it to validation.

func ComputePresence

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

ComputePresence analyzes raw JSON and returns a PresenceMap of present field paths. It enables partial validation where only provided fields are validated.

It returns an empty map (not nil) if rawJSON is empty. It has a maximum recursion depth of 100 to prevent stack overflow from deeply nested JSON structures.

Example:

rawJSON := []byte(`{"user": {"name": "Alice", "age": 0}}`)
presence, err := ComputePresence(rawJSON)
// Returns: {"user": true, "user.name": true, "user.age": true}

Errors:

  • Returns error if rawJSON is not valid JSON
Example

ExampleComputePresence demonstrates how to compute presence map from JSON.

package main

import (
	"fmt"

	"rivaas.dev/validation"
)

func main() {
	rawJSON := []byte(`{
		"user": {
			"name": "Alice",
			"age": 30
		},
		"items": [
			{"id": 1, "name": "Item 1"},
			{"id": 2}
		]
	}`)

	presence, err := validation.ComputePresence(rawJSON)
	if err != nil {
		if _, printErr := fmt.Printf("Error: %v\n", err); printErr != nil {
			panic(printErr)
		}

		return
	}

	// Check if specific paths are present
	if _, err = fmt.Printf("user.name present: %v\n", presence.Has("user.name")); err != nil {
		panic(err)
	}
	if _, err = fmt.Printf("user.email present: %v\n", presence.Has("user.email")); err != nil {
		panic(err)
	}
	if _, err = fmt.Printf("items.0.name present: %v\n", presence.Has("items.0.name")); err != nil {
		panic(err)
	}
	if _, err = fmt.Printf("items.1.name present: %v\n", presence.Has("items.1.name")); err != nil {
		panic(err)
	}

	// Get leaf paths (fields that aren't prefixes of others)
	leaves := presence.LeafPaths()
	// Sort for consistent output in example
	if _, err = fmt.Printf("Leaf paths count: %d\n", len(leaves)); err != nil {
		panic(err)
	}
	if _, err = fmt.Printf("Sample leaf: %s\n", leaves[0]); err != nil {
		panic(err)
	}

}
Output:

user.name present: true
user.email present: false
items.0.name present: true
items.1.name present: false
Leaf paths count: 5
Sample leaf: items.0.id

func (PresenceMap) Has

func (pm PresenceMap) Has(path string) bool

Has returns true if the exact path is present.

func (PresenceMap) HasPrefix

func (pm PresenceMap) HasPrefix(prefix string) bool

HasPrefix returns true if any path with the given prefix is present. HasPrefix is useful for checking if a nested object or array element is present.

func (PresenceMap) LeafPaths

func (pm PresenceMap) LeafPaths() []string

LeafPaths returns paths that aren't prefixes of others. LeafPaths is useful for partial validation where only leaf fields that were actually provided should be validated, not their parent objects.

Example:

  • If presence contains "address" and "address.city", only "address.city" is a leaf.
  • If presence contains "items.0" and "items.0.name", only "items.0.name" is a leaf.

type Redactor

type Redactor func(path string) bool

Redactor is a function that determines if a field should be redacted in error messages. It returns true if the field at the given path should have its value hidden. Use WithRedactor to configure a redactor for a Validator.

Example:

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

type Strategy

type Strategy int

Strategy defines the validation approach to use. Use WithStrategy to set a strategy, or leave as StrategyAuto for automatic selection.

const (
	// StrategyAuto automatically selects the best strategy based on the type.
	// Priority: Interface methods > Struct tags > JSON Schema
	StrategyAuto Strategy = iota

	// StrategyTags uses struct tag validation (go-playground/validator).
	StrategyTags

	// StrategyJSONSchema uses JSON Schema validation.
	StrategyJSONSchema

	// StrategyInterface calls Validate() or ValidateContext() methods.
	StrategyInterface
)

type Validator

type Validator struct {
	// contains filtered or unexported fields
}

Validator provides struct validation with configurable options.

Use New or MustNew to create a configured Validator, or use package-level functions (Validate, ValidatePartial) for zero-configuration validation.

Validator supports three validation strategies (see Strategy):

Validator is safe for concurrent use by multiple goroutines.

Example:

validator := validation.MustNew(
    validation.WithRedactor(sensitiveRedactor),
    validation.WithMaxErrors(10),
)

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

func MustNew added in v0.2.0

func MustNew(opts ...Option) *Validator

MustNew creates a Validator with the given options. Panics if configuration is invalid.

Use in main() or init() where panic on startup is acceptable.

Example:

validator := validation.MustNew(
    validation.WithRedactor(sensitiveRedactor),
    validation.WithMaxErrors(10),
)
Example

ExampleMustNew demonstrates creating a Validator with MustNew (panics on error).

package main

import (
	"context"
	"fmt"

	"rivaas.dev/validation"
)

func main() {
	// MustNew panics if configuration is invalid - suitable for use in main() or init()
	validator := validation.MustNew(
		validation.WithMaxErrors(10),
	)

	type User struct {
		Name string `json:"name" validate:"required"`
	}

	user := User{Name: "Alice"}
	if err := validator.Validate(context.Background(), &user); err != nil {
		if _, printErr := fmt.Printf("Validation failed: %v\n", err); printErr != nil {
			panic(printErr)
		}
	} else {
		if _, printErr := fmt.Println("User is valid"); printErr != nil {
			panic(printErr)
		}
	}
}
Output:

User is valid

func New added in v0.2.0

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

New creates a Validator with the given options. New returns an error if configuration is invalid (e.g., negative maxErrors).

See Option for available configuration options.

Example:

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

ExampleNew demonstrates creating a configured Validator instance.

package main

import (
	"context"
	"fmt"

	"rivaas.dev/validation"
)

func main() {
	// Create a validator with custom configuration
	validator, err := validation.New(
		validation.WithMaxErrors(5),
		validation.WithRedactor(func(path string) bool {
			return path == "password" || path == "token"
		}),
	)
	if err != nil {
		if _, printErr := fmt.Printf("Failed to create validator: %v\n", err); printErr != nil {
			panic(printErr)
		}

		return
	}

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

	user := User{Email: "[email protected]"}
	if validateErr := validator.Validate(context.Background(), &user); validateErr != nil {
		if _, err = fmt.Printf("Validation failed: %v\n", validateErr); err != nil {
			panic(err)
		}
	} else {
		if _, err = fmt.Println("Validation passed"); err != nil {
			panic(err)
		}
	}
}
Output:

Validation passed

func (*Validator) Validate

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

Validate validates a value using this validator's configuration.

Validate returns nil if validation passes, or an *Error if validation fails. The error can be type-asserted to *Error for structured field errors. Per-call options override the validator's base configuration.

Parameters:

  • ctx: Context passed to ValidatorWithContext implementations
  • val: The value to validate (typically a pointer to a struct)
  • opts: Optional per-call configuration overrides (see Option)

Example:

validator := validation.MustNew(validation.WithMaxErrors(10))

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

ExampleValidator_Validate demonstrates using a Validator instance.

package main

import (
	"context"
	"errors"
	"fmt"

	"rivaas.dev/validation"
)

func main() {
	validator := validation.MustNew()

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

	user := User{Email: "invalid-email"}
	err := validator.Validate(context.Background(), &user)
	if err != nil {
		var verr *validation.Error
		if errors.As(err, &verr) {
			if _, printErr := fmt.Printf("Found %d error(s)\n", len(verr.Fields)); printErr != nil {
				panic(printErr)
			}
			if _, printErr := fmt.Printf("First error: %s\n", verr.Fields[0].Message); printErr != nil {
				panic(printErr)
			}
		}
	}
}
Output:

Found 1 error(s)
First error: must be a valid email address

func (*Validator) ValidatePartial added in v0.2.0

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

ValidatePartial validates only fields present in the PresenceMap. It is useful for PATCH requests where only provided fields should be validated. Use ComputePresence to create a PresenceMap from raw JSON.

type ValidatorInterface added in v0.2.0

type ValidatorInterface interface {
	Validate() error
}

ValidatorInterface is the interface for custom validation methods. When implemented by a struct, it causes Validate() to be called during validation.

Note: This interface is named ValidatorInterface to avoid confusion with the Validator struct which is the main validation engine.

Example:

type User struct {
    Email string `json:"email"`
}

func (u *User) Validate() error {
    if !strings.Contains(u.Email, "@") {
        return errors.New("email must contain @")
    }
    return nil
}

type ValidatorWithContext

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

ValidatorWithContext interface for context-aware validation methods. It is preferred over ValidatorInterface when a context is available, as it allows for tenant-specific rules, request-scoped data, etc.

Example:

type User struct {
    Email string `json:"email"`
}

func (u *User) ValidateContext(ctx context.Context) error {
    userID := ctx.Value("user_id").(string)
    // Use context data for validation (e.g., tenant-specific rules)
    return nil
}

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL