Rene Nochebuena 328b80c060 feat(valid): initial stable release v0.9.0
Struct validation backed by go-playground/validator/v10 with xerrors integration and pluggable i18n message providers.

What's included:
- Validator interface with Struct(v any) error method
- New(...Option) constructor with WithMessageProvider functional option
- MessageProvider interface for i18n; DefaultMessages (EN) and SpanishMessages (ES) built in
- ValidationErrors mapped to xerrors.ErrInvalidInput with field and tag context keys
- InvalidValidationError (non-struct input) mapped to xerrors.ErrInternal
- Full playground.ValidationErrors attached via WithError for callers needing all failures

Tested-via: todo-api POC integration
Reviewed-against: docs/adr/
2026-03-18 21:02:26 +00:00

valid

Struct validation backed by go-playground/validator with structured error output.

Module: code.nochebuena.dev/go/valid Tier: 1 — depends on xerrors (Tier 0) and go-playground/validator/v10 Go: 1.25+ Dependencies: code.nochebuena.dev/go/xerrors, github.com/go-playground/validator/v10


Overview

valid wraps go-playground/validator/v10 and maps validation failures to structured *xerrors.Err values. It replaces the old check package, adding a plain constructor, a swappable message provider, and English messages by default.

Installation

go get code.nochebuena.dev/go/valid

Quick start

import "code.nochebuena.dev/go/valid"

type CreateUserRequest struct {
    Name  string `validate:"required"`
    Email string `validate:"required,email"`
    Age   int    `validate:"min=18,max=120"`
}

v := valid.New()

err := v.Struct(req)
if err != nil {
    // err is a *xerrors.Err with code ErrInvalidInput.
    var xe *xerrors.Err
    if errors.As(err, &xe) {
        fmt.Println(xe.Code())        // INVALID_ARGUMENT
        fmt.Println(xe.Message())     // "field 'Email' must be a valid email address"
        fmt.Println(xe.Fields())      // map[field:Email tag:email]
    }
}

Usage

Creating a validator

// English messages (default).
v := valid.New()

// Spanish messages.
v := valid.New(valid.WithMessageProvider(valid.SpanishMessages))

// Custom message provider.
v := valid.New(valid.WithMessageProvider(myProvider))

Validating structs

err := v.Struct(myStruct)
Outcome Error returned
All fields pass nil
Field constraint failure *xerrors.Err with ErrInvalidInput, first error only
Not a struct *xerrors.Err with ErrInternal

The returned *xerrors.Err for field failures carries:

  • Fields()["field"] — the failing struct field name
  • Fields()["tag"] — the failing validation rule (e.g. "email", "required")
  • Unwrap() — the underlying validator.ValidationErrors

Message providers

MessageProvider maps a validation failure to a human-readable message:

type MessageProvider interface {
    Message(field, tag, param string) string
}

Built-in presets:

Variable Language Usage
DefaultMessages English automatic (no option needed)
SpanishMessages Spanish WithMessageProvider(valid.SpanishMessages)

Custom provider example:

type myMessages struct{}

func (m myMessages) Message(field, tag, param string) string {
    switch tag {
    case "required":
        return field + " is mandatory"
    default:
        return field + " failed: " + tag
    }
}

v := valid.New(valid.WithMessageProvider(myMessages{}))

Design decisions

No singletonvalid.New(opts...) returns a plain value. Multiple validators with different configurations can coexist. Tests create isolated instances without global state.

Only the first validation error is surfacedgo-playground/validator returns all field errors at once; we surface only the first for API simplicity. Apps needing all failures can cast errors.Unwrap(err) to validator.ValidationErrors:

var xe *xerrors.Err
errors.As(err, &xe)

var ve validator.ValidationErrors
errors.As(errors.Unwrap(xe), &ve) // all field errors

valid imports xerrors directlyvalid is Tier 1 and xerrors is Tier 0. The import is intentional; valid constructs *xerrors.Err values. Duck-typing is reserved for cases where the import would create a circular or cross-tier dependency.

Spanish is bundled, not a separate module — the Spanish preset is a small, zero-dep addition. Splitting it into a separate module would add publish overhead for negligible gain.

Ecosystem

Tier 0:   xerrors
               ↑ (direct import — constructs *xerrors.Err)
Tier 1:   valid ← you are here
               ↑
Tier 2:   httputil (injects valid.Validator into generic handler)

License

MIT

Description
Struct validator backed by go-playground/validator with xerrors integration and i18n.
Readme 41 KiB
2026-03-18 15:03:13 -06:00
Languages
Go 100%