Skip to content

Navigation Menu

Sign in
Sign up

Declaring Schemas

wiki edited this page Sep 4, 2026 · 1 revision

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.

Returning nil

Means
RequestBody() → nil the route accepts no request body
Responses() → nil skip response validation entirely for this route

Composition

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, merged

Pass 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.

Per-status responses

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.

A terser shape

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.

How the schemas are read

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 BaseURL handling was broken on top of that.

Where schemas live

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.

Clone this wiki locally

AltStyle によって変換されたページ (->オリジナル) /