Files
valid/CLAUDE.md
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

3.3 KiB

valid

Struct validation via go-playground/validator, returning xerrors-typed errors and supporting pluggable i18n messages.

Purpose

Wraps github.com/go-playground/validator/v10 behind a minimal Validator interface. Translates playground error types into *xerrors.Err values with stable Code values (ErrInvalidInput, ErrInternal), so HTTP middleware can map validation failures to HTTP status codes without knowing anything about the underlying library.

Tier & Dependencies

Tier 1 — depends on:

  • code.nochebuena.dev/go/xerrors (Tier 0, error types)
  • github.com/go-playground/validator/v10 (external, hidden behind the interface)

Key Design Decisions

  • Playground validator as hidden backend (ADR-001): *playground.Validate is never exposed in the public API. Callers interact only with the Validator interface and *xerrors.Err errors.
  • xerrors integration (ADR-002): ValidationErrorsErrInvalidInput; InvalidValidationError (non-struct arg) → ErrInternal. Only the first failing field is surfaced; the full playground.ValidationErrors is attached via WithError for callers that need all failures.
  • MessageProvider for i18n (ADR-003): Human-readable messages are delegated to a MessageProvider interface. DefaultMessages (English) is used automatically. SpanishMessages is opt-in. Custom providers are accepted via WithMessageProvider.

Patterns

Default (English):

v := valid.New()
if err := v.Struct(req); err != nil {
    // err is *xerrors.Err with Code() == xerrors.ErrInvalidInput
}

Spanish messages:

v := valid.New(valid.WithMessageProvider(valid.SpanishMessages))

Custom message provider:

type myMessages struct{}
func (m myMessages) Message(field, tag, param string) string { ... }

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

Accessing structured context from the error:

var xe *xerrors.Err
if errors.As(err, &xe) {
    field := xe.Fields()["field"] // e.g. "Email"
    tag   := xe.Fields()["tag"]   // e.g. "email"
}

Accessing all validation errors:

var xe *xerrors.Err
errors.As(err, &xe)
var ve playground.ValidationErrors
errors.As(errors.Unwrap(xe), &ve)
// iterate ve for all failing fields

What to Avoid

  • Do not expose *playground.Validate or playground.ValidationErrors from any new public function. Keep the backend hidden.
  • Do not add generics to the Validator interface. The Struct(v any) error signature is intentionally non-generic; the playground library uses reflection internally.
  • Do not register global custom validators on the shared *playground.Validate instance — that would break isolation between callers that share a process but expect different validation behaviour.
  • Do not construct a new *playground.Validate per call; New() creates one instance per Validator.

Testing Notes

  • valid_test.go covers: New() defaults, custom MessageProvider injection, valid struct (nil error), required/email/min/max failures, non-struct input (ErrInternal), Fields() context keys, Unwrap to playground.ValidationErrors, Spanish messages, and all built-in message tags.
  • compliance_test.go checks at compile time that valid.New() satisfies Validator, and that DefaultMessages/SpanishMessages satisfy MessageProvider.
  • No network or database access — all tests are pure in-process.