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/
3.3 KiB
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.Validateis never exposed in the public API. Callers interact only with theValidatorinterface and*xerrors.Errerrors. - xerrors integration (ADR-002):
ValidationErrors→ErrInvalidInput;InvalidValidationError(non-struct arg) →ErrInternal. Only the first failing field is surfaced; the fullplayground.ValidationErrorsis attached viaWithErrorfor callers that need all failures. - MessageProvider for i18n (ADR-003): Human-readable messages are delegated to a
MessageProviderinterface.DefaultMessages(English) is used automatically.SpanishMessagesis opt-in. Custom providers are accepted viaWithMessageProvider.
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.Validateorplayground.ValidationErrorsfrom any new public function. Keep the backend hidden. - Do not add generics to the
Validatorinterface. TheStruct(v any) errorsignature is intentionally non-generic; the playground library uses reflection internally. - Do not register global custom validators on the shared
*playground.Validateinstance — that would break isolation between callers that share a process but expect different validation behaviour. - Do not construct a new
*playground.Validateper call;New()creates one instance perValidator.
Testing Notes
valid_test.gocovers:New()defaults, customMessageProviderinjection, valid struct (nil error),required/email/min/maxfailures, non-struct input (ErrInternal),Fields()context keys,Unwraptoplayground.ValidationErrors, Spanish messages, and all built-in message tags.compliance_test.gochecks at compile time thatvalid.New()satisfiesValidator, and thatDefaultMessages/SpanishMessagessatisfyMessageProvider.- No network or database access — all tests are pure in-process.