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/
valid
Struct validation backed by
go-playground/validatorwith 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 nameFields()["tag"]— the failing validation rule (e.g."email","required")Unwrap()— the underlyingvalidator.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 singleton — valid.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 surfaced — go-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 directly — valid 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