Resolve JSON tag name resolution roadmap item: field names in error context now use the json struct tag when available, falling back to the Go field name. Commits MessageProvider interface as stable. Bumps xerrors dependency from v0.9.0 to v1.0.0. API committed as stable.
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