Skip to content

Reading Validated Data

The handler reads what the validator produced. ctx.get.validated() returns the stored output for a schema, and a direct contract call validates a value on the spot.

Reading Stored Output

ctx.get.validated() returns the validated data keyed by source. The types come from the schema definition, so the handler gets full type safety for every field:

typescript
export function 
POST
(
ctx
:
Context
): Response {
// Typed output, already validated const {
body
} =
ctx
.
get
.
validated
<typeof
createUser
>()
return
ctx
.
send
.
json
({
created
:
body
.
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 data is covered in Validator Middleware.

Reading Without A Validator Throws

ctx.get.validated() expects the Validator Middleware to have run first. Calling it with no validated data throws Deno.errors.NotSupported, 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, so the framework maps it to a 501 Not Implemented through the same error handling path that maps every other thrown error.

Checking Params In A Handler

Route params resolve after middleware runs, so the Validator Middleware does not accept a params source. The handler validates them directly by calling the contract function:

typescript
export function 
GET
(
ctx
:
Context
): Response {
// Validate the matched route param const {
id
} =
UserId
(
ctx
.
get
.
param
())
return
ctx
.
send
.
json
({
id
})
}

Calling a contract directly returns the transform 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.cause as 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.

How a validation throw maps to a status: an error that already carries a status passes through, an error with reasons becomes a 422 keeping those reasons, any other client throw becomes a generic 422, and reading with no validator registered throws Deno.errors.NotSupported that maps to a 501 because that signals a wiring mistake rather than bad input

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:

typescript
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
as
HttpStatusCode
}
) })

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 as validate:failed events, so a listener can record them, covered in Error Reporting.

Where To Go Next

Released under the MIT License.