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/
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:
*playground.InvalidValidationError— the argument passed toStruct()is not a struct (programmer error).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 onxe.Code()to produce400 Bad RequestforErrInvalidInputwithout 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
ValidationErrorsslice 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.