-
Notifications
You must be signed in to change notification settings - Fork 0
Request Validation
Validation is go-playground/validator/v10, driven by validate tags:
type CreateUserRequest struct {
Email string `json:"email" validate:"required,email"`
Name string `json:"name" validate:"required,min=2,max=100"`
Age int `json:"age" validate:"gte=18,lte=120"`
Website string `json:"website" validate:"omitempty,url"`
Role string `json:"role" validate:"required,oneof=admin user guest"`
Tags []string `json:"tags" validate:"max=10,dive,min=1"`
Address *Address `json:"address" validate:"omitempty"`
}
type Address struct {
Street string `json:"street" validate:"required"`
Postal string `json:"postal" validate:"required,len=5,numeric"`
}Nested structs are validated automatically. dive applies the rules after it to
each element of a slice or map.
Common tags: required, omitempty, email, url, uuid, min/max,
len, gte/lte, oneof, eqfield, numeric, alphanum. The full set is
in the validator/v10 documentation.
{
"type": "urn:rex:problem:validation-failed",
"title": "Unprocessable Entity",
"status": 422,
"detail": "the request body failed validation",
"errors": [
{"field": "email", "rule": "email", "message": "must be a valid email address"},
{"field": "age", "rule": "gte", "message": "must be at least 18", "value": "12"}
]
}Content-Type: application/problem+json.
-
fieldis the JSON name, not the Go field name — it is the client's own term for the thing. -
ruleis the constraint that failed. It used to be calledtag, which described the Go struct tag that produced the constraint rather than the constraint itself. -
valueis omitted for anything that could be a credential.
errors is an RFC 9457 extension member, which §3.2 permits; a consumer that
does not know it ignores it.
func createUser(ctx rxroute.Context) {
body, ok := validation.GetRequestBody[CreateUserRequest](ctx.Request())
if !ok {
// Unreachable when the route declares a request schema — the
// middleware would have answered 422 already.
rextension.WriteProblem(ctx.ResponseWriter(), ctx.Request(),
400, rextension.ProblemBadRequest, "a request body is required")
return
}
_ = ctx.JSON(201, save(body))
}The type parameter must match the type in the schema. A mismatch returns the
zero value and false, not a panic.
By default, a body carrying a member the schema does not declare is a 422.
{"email": "a@b.com", "nmae": "Ada"}{
"type": "urn:rex:problem:validation-failed",
"status": 422,
"detail": "the request body failed validation",
"errors": [{"field": "nmae", "rule": "unknown", "message": "unrecognised field"}]
}The field name comes from the decoder's JSON pointer, not from its error text. The text reads:
json: cannot unmarshal JSON string into Go api.CreateUserRequest: unknown object member name "nmae"
which names an internal Go type — that belongs in a log, not in a response. The
member name is the client's own input, so echoing it is both safe and the only
genuinely useful part. validation.UnknownFieldName(err) is the helper that
extracts it.
An unknown member is almost always a client mistake: a misspelled field, a stale integration, a field renamed on one side only. Accepting it silently is what lets that mistake reach production looking like a success — the request returns 200, the field is dropped, and nobody learns anything until the missing data is noticed downstream.
The configuration field is phrased as a negative (AllowUnknownFields) so
permitting them is something you have to write. That also matters because a
partial struct literal is used verbatim: the zero value has to be the safe one,
or &Config{Codecs: …} would quietly opt out.
validation.NewConfig(validation.WithAllowUnknownFields())A real weakening, for exactly the reason above. Do it for an established client you cannot change.
type StrictCodec interface {
Codec
UnmarshalStrict(data []byte, v interface{}) error
}With rejection enabled, a registered codec that does not implement it fails the boot:
validation: codec "application/xml" cannot reject unknown members.
Implement StrictCodec.UnmarshalStrict, or enable WithAllowUnknownFields().
Decoding leniently while the configuration says members are rejected is the
"configured but inoperative" failure this whole interface exists to avoid —
invisible in exactly the way that matters: requests succeed, so nothing looks
wrong. validation.ValidateStrictness(codecs) is the check, and the error names
both remedies because either is legitimate.
Body limits are the router's job, not this extension's. A body over the
limit is a 413 from the router before the validation middleware sees it. See
rex → TLS and listener limits.
For rules that are not expressible as tags, validate in the handler after decoding and answer with a problem document:
if body.StartsAt.After(body.EndsAt) {
rextension.NewProblem(422, rextension.ProblemValidationFailed,
"the request body failed validation").
WithErrors(rextension.FieldError{
Field: "starts_at", Rule: "before", Message: "must be before ends_at",
}).
Write(ctx.ResponseWriter(), ctx.Request())
return
}Using the same document shape keeps the client parsing one format.
rextension-validation — request/response validation and content negotiation for Rex · MIT · © 2026 Kryovyx