-
Notifications
You must be signed in to change notification settings - Fork 0
Declaring Schemas
type ValidatableRoute interface { // = rextension.BodySchemaProvider
RequestBody() BodySchema
Responses() map[int]BodySchema
}Implement it on a route. A route that does not is passed through without validation and documented without schemas.
type CreateUser struct{ rxroute.Route }
func (r *CreateUser) RequestBody() validation.BodySchema {
return validation.Scalar(CreateUserRequest{})
}
func (r *CreateUser) Responses() map[int]validation.BodySchema {
return map[int]validation.BodySchema{
201: validation.Scalar(UserResponse{}),
422: validation.Scalar(rextension.Problem{}),
}
}
app.RegisterRoute(&CreateUser{Route: rxroute.New("POST", "/users", createUser)})⚠ With a pointer receiver, register the route as a pointer or the assertion fails and the route is silently unvalidated.
| Means | |
|---|---|
RequestBody() → nil |
the route accepts no request body |
Responses() → nil |
skip response validation entirely for this route |
validation.Scalar(CreateUserRequest{}) // one type
validation.OneOf(CardPayment{}, BankPayment{}) // exactly one must match
validation.AnyOf(EmailContact{}, SMSContact{}) // one or more may match
validation.AllOf(BaseEvent{}, OrderPayload{}) // all, mergedPass zero-value struct literals — the types are what matter, not the values.
OneOf is the useful one for a discriminated payload:
func (r *CreatePayment) RequestBody() validation.BodySchema {
return validation.OneOf(CardPayment{}, BankTransfer{}, WalletPayment{})
}The body must validate against exactly one. AnyOf accepts a body matching at
least one; AllOf requires it to satisfy all of them at once, which is how you
express a base type plus an extension.
These map directly onto the OpenAPI keywords of the same names, so the generated document says what the validator enforces.
func (r *GetUser) Responses() map[int]validation.BodySchema {
return map[int]validation.BodySchema{
200: validation.Scalar(UserResponse{}),
404: validation.Scalar(rextension.Problem{}),
500: validation.Scalar(rextension.Problem{}),
}
}Documenting the error statuses is worth the two lines: it is what makes the generated OpenAPI document usable, and with strict responses it is what stops an undocumented status reaching a client.
Declaring schemas on every route type gets repetitive. Wrap instead:
type schemaRoute struct {
rxroute.Route
req validation.BodySchema
resp map[int]validation.BodySchema
}
func (r *schemaRoute) RequestBody() validation.BodySchema { return r.req }
func (r *schemaRoute) Responses() map[int]validation.BodySchema { return r.resp }
func withSchema(rt rxroute.Route, req validation.BodySchema, resp map[int]validation.BodySchema) rxroute.Route {
return &schemaRoute{Route: rt, req: req, resp: resp}
}
app.RegisterRoute(withSchema(
rxroute.New("POST", "/users", createUser),
validation.Scalar(CreateUserRequest{}),
map[int]validation.BodySchema{201: validation.Scalar(UserResponse{})},
))Combine it with the security and health wrappers by embedding all of them on one route type — the interfaces are independent.
ValidationFactory is a
PerRouteMiddleware
factory: the framework calls it once per route at freeze, the schemas are
read there, and the closure captures them. A route with no schemas gets nil —
no middleware, no cost.
The middleware used to look the route up per request in an index keyed at registration by the route's pattern, against the live URL path. Those never match for a parameterized route, so the lookup always missed — and the code fell back to reading the matched route from the context, which is why validation still worked. The index was doing nothing except costing a map lookup and a lock on every request, and its
BaseURLhandling was broken on top of that.
The contract is declared in rextension, and everything here is an alias of it.
That matters because the OpenAPI generator reads the same interface. It
previously reached these methods with reflect.MethodByName("RequestBody") and
type-asserted the result to []interface{} — unchecked, so an unexpected slice
type panicked inside document generation. Sharing the type removes the need for
all of it.
You can write rextension.Scalar or validation.Scalar interchangeably.
rextension-validation — request/response validation and content negotiation for Rex · MIT · © 2026 Kryovyx