Skip to content

Validation

@body, @query, @headers, and @param accept an optional Standard Schema (the ~standard interface shared by zod, valibot, arktype, and others). green-tea’s core has zero runtime dependency on any of them — bring whichever validator you already use.

import { z } from 'zod';
import { Route, Post, body } from '@green-tea/core';
const CreateUser = z.object({ email: z.string().email() });
@Route('/users')
class UserController {
@Post('/')
create(@body(CreateUser) user: { email: string }) {
return { created: user.email };
}
}

Parsed value replaces the argument; ctx stays raw

Section titled “Parsed value replaces the argument; ctx stays raw”

The value the handler receives is the schema’s parsed/coerced output, not the raw input — ctx.body (via @ctx() or a @needs-fed step) stays exactly what the transport parsed. Query strings are always strings on the wire, so a schema is often the coercion point:

const ListQuery = z.object({ page: z.coerce.number() });
@Get('/list')
list(@query(ListQuery) q: { page: number }) {
return { page: q.page, isNum: typeof q.page === 'number' }; // GET /list?page=2 → true
}

See Argument decorators for the full slot semantics of each decorator.

A failing schema short-circuits the request with 422:

{ "error": "Validation failed", "source": "body", "issues": [{ "path": "email", "message": "Invalid email" }] }

source is which envelope failed ('body' | 'query' | 'params' | 'headers'); issues is the schema’s issues flattened to { path, message } (path is dot-joined).

  • Fail-fast. Arguments validate in order; the first failing one throws — later args in the same handler are not checked in that request.
  • Steps see raw input. Only the resolved handler argument is coerced/validated; a @Step reading the same data via ctx.body/ctx.query/etc. always sees the untouched value.
  • A throwing schema is a 500, not a 422. The Standard Schema contract says validate() returns { issues } on failure — it isn’t supposed to throw. If it does anyway, that propagates as an uncaught error (→ 500), since it signals a bug in the schema, not user input.
  • Async schemas are awaitedvalidate() returning a Promise works transparently.