/**
* Schema validation via the {@link https://standardschema.dev Standard Schema}
* interface. Any validator that implements it — Zod, Valibot, ArkType, TypeBox
* (with its adapter), etc. — can be used to validate a route's `body`, `query`,
* `params`, and `response` without turnover depending on any of them.
*
* The `StandardSchemaV1` types below are a structural copy of the spec (v1), as
* the spec recommends, so no runtime dependency is pulled in.
*/
/**
* A validator implementing the Standard Schema v1 interface — the only shape
* turnover requires of a schema, so any conforming library is accepted without
* an import. Used wherever a route or config declares input/output schemas.
*/
export interface StandardSchemaV1 {
/** The spec's single tilde-prefixed key (chosen to avoid colliding with a library's own fields). */
readonly '~standard': StandardSchemaProps
}
/** The object under the `'~standard'` key: version, vendor, and the `validate` entry point. */
export interface StandardSchemaProps {
/** The version of the Standard Schema (always `1`). */
readonly version: 1
/** The name of the schema library (e.g. `'zod'`). */
readonly vendor: string
/** The sole runtime entry point turnover calls; may be sync or async, and never throws — it reports failure as `issues`. */
readonly validate: (
value: unknown,
) => StandardResult | Promise>
/** Inference-only carrier of the schema's input and output types. */
readonly types?: StandardTypes | undefined
}
/** The result of validation: {@link StandardSuccess} or {@link StandardFailure}. */
export type StandardResult = StandardSuccess | StandardFailure
/** A successful validation result. */
export interface StandardSuccess {
/** The validated (and possibly transformed) value. */
readonly value: Output
/** Always `undefined` on success (discriminates the result union). */
readonly issues?: undefined
}
/** A failed validation result. */
export interface StandardFailure {
/** The non-empty list of validation issues. */
readonly issues: ReadonlyArray
}
/** A single validation issue. */
export interface StandardIssue {
/** The human-readable error message; surfaced verbatim in the `422` response's `issues[].message`. */
readonly message: string
/** Location of the offending value, if applicable; segments may be bare keys or `{ key }` objects — {@link issuePath} normalizes both. */
readonly path?: ReadonlyArray | undefined
}
/** The object form of a {@link StandardIssue} path segment (some libraries emit bare `PropertyKey`s instead). */
export interface StandardPathSegment {
/** The property key this segment points at. */
readonly key: PropertyKey
}
/** The input and output types of a schema, present for inference only. */
export interface StandardTypes {
/** The type accepted before validation/coercion (what a client sends). */
readonly input: Input
/** The type produced after validation/coercion (what lands on `ctx.valid`). */
readonly output: Output
}
/** Infer the input type a schema accepts. */
export type InferInput = NonNullable<
Schema['~standard']['types']
>['input']
/** Infer the output type a schema produces after validation. */
export type InferOutput = NonNullable<
Schema['~standard']['types']
>['output']
/**
* Standard Schemas a route can declare for its inputs and output. Declared on a
* route decorator's options (`@post("/", { body, query, params, response })`).
* Inputs are validated after guards, in the order `params` → `query` → `body`;
* an omitted field is not validated.
*/
export interface RouteSchemas {
/**
* Validates the parsed request body (checked last of the three inputs). The
* coerced output lands on `ctx.valid.body`, while `ctx.body()` still returns
* the raw body. A failure throws `422` with code `validation_failed`.
*/
body?: StandardSchemaV1
/**
* Validates the query string after it is flattened to an object (repeated
* keys become arrays). Coerced output lands on `ctx.valid.query`; a failure
* throws `422` with code `validation_failed`.
*/
query?: StandardSchemaV1
/**
* Validates the path params (checked first). Coerced output lands on
* `ctx.valid.params`; a failure throws `422` with code `validation_failed`.
*/
params?: StandardSchemaV1
/**
* Validates the handler's return value. Unlike the inputs, a mismatch is
* treated as a server bug — it is logged and rendered as an opaque `500`,
* never surfaced to the client as a `422`.
*/
response?: StandardSchemaV1
}
/**
* Validate `value` against a Standard Schema, awaiting the validator whether it
* is sync or async. Unlike route input validation, this never throws — a
* failure is returned as `{ issues }` for you to inspect, not raised as a `422`.
*
* @typeParam Schema - The Standard Schema being validated against; its `InferOutput` types the returned `value`.
* @param schema - The Standard Schema to validate against.
* @param value - The unknown value to validate.
* @returns The result: `{ value }` (coerced output) on success, or `{ issues }` on failure.
*/
export async function validate(
schema: Schema,
value: unknown,
): Promise>> {
return (await schema['~standard'].validate(value)) as StandardResult<
InferOutput
>
}
/**
* Normalize a {@link StandardIssue} path into a flat array of keys, collapsing
* both segment forms the spec permits — bare `PropertyKey`s and `{ key }`
* objects — to their key. This is the shape used in the `422` response's
* `issues[].path`.
*
* @param issue - The validation issue whose (possibly mixed-form) path to normalize.
* @returns The path as a flat array of keys, or `undefined` if the issue carries none.
*/
export function issuePath(
issue: StandardIssue,
): Array | undefined {
return issue.path?.map((seg) => (typeof seg === 'object' ? seg.key : seg))
}