Files
valid/docs/adr/ADR-002-xerrors-integration.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

2.8 KiB

ADR-002: xerrors Integration for Validation Errors

Status: Accepted Date: 2026-03-18

Context

Validation failures need to be distinguishable from other error categories (not found, internal, already exists) so that HTTP handlers can map them to the correct status codes without inspecting error messages. The xerrors module provides a typed error system with stable Code values for this purpose.

The go-playground/validator library returns two distinct error types for failures:

  1. *playground.InvalidValidationError — the argument passed to Struct() is not a struct (programmer error).
  2. playground.ValidationErrors — one or more field constraints failed (expected user input error).

These two cases must be mapped to different xerrors codes: the first is an internal bug, the second is a bad-input condition.

Decision

Struct(v any) error applies the following mapping:

playground error type xerrors code rationale
*InvalidValidationError ErrInternal Passing a non-struct is a programming error
ValidationErrors (first) ErrInvalidInput Field constraint failures are user errors
any other non-nil error ErrInternal Unexpected; treated as internal

Only the first ValidationErrors entry is surfaced. The full playground.ValidationErrors slice is attached as the wrapped error via WithError(err), so callers who need all failures can use errors.Unwrap to reach playground.ValidationErrors and iterate themselves.

The *xerrors.Err for ErrInvalidInput includes two context fields set via WithContext:

  • "field": the struct field name (e.g. "Email")
  • "tag": the failing rule (e.g. "email", "required")

These allow structured logging and API response builders to attach field-level detail without parsing the error message string.

Consequences

  • Positive: HTTP middleware can use errors.As(err, &xe) and switch on xe.Code() to produce 400 Bad Request for ErrInvalidInput without any knowledge of the validation library.
  • Positive: Structured context fields (field, tag) are available for logging and response serialization without string parsing.
  • Positive: The full ValidationErrors slice is recoverable for callers that must report all failing fields (e.g., form validation responses).
  • Negative: Only the first error is surfaced by default. Callers expecting a list of all failures in the top-level error must unwrap manually. This is a deliberate trade-off: the common case (return the first problem to the user) is easy; the less common case (return all problems) is possible but requires extra code.