Reading Validated Data
The handler reads what the validator produced. Validator.read returns the stored output for a schema, and Validator.check validates a value on the spot.
Reading Stored Output
Validator.read<typeof schema>(ctx) returns the validated data keyed by source. Passing the schema type gives the handler full types for every field:
export function POST(ctx: Context): Response {
// Typed output, already validated
const { json } = Validator.read<typeof createUser>(ctx)
return ctx.send.json({ created: json.name })
}The shape mirrors the schema, so a schema with query and headers returns both keys with their own contract output types. The middleware that stored this state is covered in Validator Middleware.
Reading Without A Validator Throws 500
Validator.read expects the Validator Middleware to have run first. Calling it with no validated state throws a 500, since reaching a read with nothing stored means the middleware was never registered. This is a wiring mistake in the code, not bad input from a client.
export function POST(ctx: Context): Response {
// Throws 500 if Mware.validator never ran
const { json } = Validator.read<typeof createUser>(ctx)
return ctx.send.json(json)
}Checking Params In A Handler
Route params resolve after middleware runs, so the Validator Middleware rejects a params source. The handler validates them directly with Validator.check(contract, value):
export function GET(ctx: Context): Response {
// Validate the matched route param
const { id } = Validator.check(UserId, ctx.params())
return ctx.send.json({ id })
}Validator.check returns the contract output when the value passes and throws when it fails, the same throw the middleware produces. It works for any value, not only params, which makes it handy for validating a slice of data mid-handler.
How Failures Surface
A contract that rejects its input throws, and the framework maps that throw to a status:
- An error that already carries a status passes through unchanged.
- An error carrying failure reasons becomes a 422 Unprocessable Content, with the reasons preserved on
error.causeas a string array. - Any other throw from client input becomes a generic 422.
Client input never turns into a 500. That guarantee keeps a malformed body, a bad query string, or a thrown guard on the client side of the status line where it belongs.

The reasons ride on error.cause, so a custom handler reads them and replies with field-level detail. Error shaping is centralized in Object Details, the single router.catch that handles validation alongside every other error:
router.catch((ctx, info) => {
// Pull validation reasons off the cause
const reasons = Array.isArray(info.error.cause)
? info.error.cause.filter((reason): reason is string => typeof reason === 'string')
: []
return ctx.send.json(
{ error: 'request_failed', status: info.statusCode, reasons },
{ status: info.statusCode }
)
})For the full ErrorInfo object and the default response when no handler is set, see Object Details and Default Behavior. Validation faults also flow through the observability bus, so a listener can record them, covered in Error Reporting.
Where To Go Next
- Define Schema - write the contracts behind the reads.
- Object Details - shape the response a failure produces.
- Advanced Patterns - validate params beside a body validator.
- Validation Overview - how the pieces fit together.