02Guides
Validation
Zod, Valibot and ArkType schemas and runtime validation of responses and requests.
TypeScript types disappear at runtime: a server that sends "id": "42" where the specification says integer still type-checks. With features.validation, swaggerz also generates a schema for every model and checks responses (and, if you want, request bodies) against them as they cross the wire.
features: { validation: 'zod' }, // or 'valibot', 'arktype'// or, with options:features: { validation: { library: 'zod', response: true, request: true } },| Library | Versions (checked by pnpm compat) |
Directory | Failure cause |
|---|---|---|---|
| Zod | 4.0.15 – 4.x | zod/ |
ZodError |
| Valibot | 1.0.0 – 1.x | valibot/ |
ValiError |
| ArkType | 2.0.0 – 2.x (TypeScript 5.4+) | arktype/ |
ArkType’s TraversalError |
What is validated
Section titled “What is validated”- Responses (
response, defaulttrue): the body of every successful response is parsed with its schema before the operation resolves. - Request bodies (
request, defaultfalse): the body is checked before it is serialized; an invalid body is never sent.
A failure rejects with a RequestError of kind 'validation' whose cause is the library’s error, with the path to the offending value:
import { getPetById, isRequestError } from './generated/api';
try { await getPetById({ petId: 10 });} catch (error) { if (isRequestError(error) && error.kind === 'validation') { console.error(`${error.operationId} returned an unexpected body`, error.cause); }}Using the schemas yourself
Section titled “Using the schemas yourself”The schemas are exported from their own barrel, next to the client. Use them for data that does not come through the client: form input, messages from a queue, a cached copy in storage.
import { PetSchema } from './generated/api/zod';
export function cachedPetName(json: string): string { const pet = PetSchema.parse(JSON.parse(json)); // throws a ZodError for anything else return pet.name;}The library is imported only by its directory and, when validation is on, by the operations. With validation off, nothing in the client depends on it.
What the schemas enforce
Section titled “What the schemas enforce”All three libraries enforce the same rules, following JSON Schema and the generated types:
- required and optional properties, nullability, enums and
const, unions and discriminators (including OpenAPI 3.2 default mappings), tuples,minLength/maxLength, numeric bounds andmultipleOf; additionalProperties: false(unknown properties are kept otherwise) and typed additional properties;- the assertions TypeScript cannot express:
not,if/then/else,dependentSchemasanddependentRequired; - recursive and mutually recursive schemas.
Where the libraries’ defaults differ from JSON Schema, the schemas follow JSON Schema: string lengths count characters rather than UTF-16 code units, a decimal multipleOf such as 0.01 tolerates floating-point rounding, and integers may exceed 2^53. The details are in generated code.
Choosing a library
Section titled “Choosing a library”- Zod is the most common choice and the one the examples use. Schemas are plain objects and methods.
- Valibot is modular: an application bundles only the validators it uses, which keeps a few validated operations small.
- ArkType definitions read like TypeScript types. It needs TypeScript 5.4 or later;
swaggerz doctorchecks it.
Switching libraries is a config change: the operations call the schemas through the same code path, so the rest of your code does not change.
Limitations
Section titled “Limitations”formatandpatternare documentation only (exceptdateanddate-timewithtypes.dates: 'Date', which are checked before they are converted).- Validation runs on what the client receives and sends; it does not check headers or parameters.
- In Valibot and ArkType, a closed model that contains itself through
allOfreads like the intersection only at its first level of nesting.
What's generated
openapi: 3.1.0info: title: Petstore version: '1.0'servers: - url: https://petstore.example.com/v1paths: /pets/{petId}: get: tags: [pets] summary: Find a pet by ID. description: Returns a single pet. operationId: getPetById parameters: - name: petId in: path required: true description: ID of the pet to return. schema: type: integer format: int64 minimum: 1 responses: '200': description: The pet. content: application/json: schema: $ref: '#/components/schemas/Pet' '404': description: No pet has this ID. content: application/json: schema: $ref: '#/components/schemas/Error' security: - api_key: []components: securitySchemes: api_key: type: apiKey in: header name: X-API-Key schemas: Pet: type: object required: [id, name] properties: id: type: integer format: int64 name: type: string example: Rex status: type: string description: Where the pet is in the store. enum: [available, pending, sold] Error: type: object required: [message] properties: message: type: stringzod/pet.ts
/* eslint-disable */// @generated by swaggerz. Do not edit: this file is overwritten on the next generation.
import { z } from 'zod';
/** Zod schema for {@link Pet}. */export const PetSchema = z.looseObject({ id: z.number().multipleOf(1), name: z.string(), status: z.enum(['available', 'pending', 'sold']).optional(),});pets/get-pet-by-id.ts
/* eslint-disable */// @generated by swaggerz. Do not edit: this file is overwritten on the next generation.
import { type RequestOptions, client } from '../client.js';import type { Error as ErrorModel } from '../models/error.js';import type { Pet } from '../models/pet.js';import { type ApiError, type RequestError, isApiError } from '../runtime/errors.js';import type { HttpStatus } from '../runtime/types.js';import { PetSchema } from '../zod/pet.js';
/** Parameters of {@link getPetById}. */export type GetPetByIdParams = { /** * ID of the pet to return. * * @format int64 * @minimum 1 */ petId: number;};
/** Response data of {@link getPetById}. */export type GetPetByIdResponse = Pet;
/** Body of an error response of {@link getPetById}. */export type GetPetByIdErrorBody = ErrorModel;
/** * The `ApiError`s {@link getPetById} rejects with: one per documented status, so checking `status` narrows `body`. * * The last member covers statuses the specification does not document. */export type GetPetByIdApiError = | ApiError<ErrorModel, 404> | ApiError<unknown, Exclude<HttpStatus<1 | 3 | 4 | 5>, 404>>;
/** Everything {@link getPetById} rejects with: an `ApiError` for an error status, or a `RequestError` when no usable response arrived. */export type GetPetByIdError = GetPetByIdApiError | RequestError;
/** * Find a pet by ID. * * Returns a single pet. * * @remarks `GET /pets/{petId}` (operationId `getPetById`) * @param params - Request parameters. * @param options - Per-request options: `signal`, `timeout`, `headers`, `client`. * @returns `200` The pet * @throws {@link ApiError} `404` No pet has this ID; narrow it with {@link isGetPetByIdApiError}. * @throws {@link RequestError} When no usable response is received (network failure, timeout or invalid body). */export function getPetById(params: GetPetByIdParams, options?: RequestOptions): Promise<GetPetByIdResponse> { return (options?.client ?? client).request<GetPetByIdResponse>( { operationId: 'getPetById', method: 'GET', path: '/pets/{petId}', pathParams: { petId: params.petId }, accept: 'application/json', security: [['api_key']], validateResponse: (data) => PetSchema.parse(data), }, options, );}
/** Whether `error` is an `ApiError` rejected by {@link getPetById}. It narrows to {@link GetPetByIdApiError}, so checking `status` afterwards narrows `body` too. */export function isGetPetByIdApiError(error: unknown): error is GetPetByIdApiError { return ( isApiError(error) && error.operationId === 'getPetById' && error.method === 'GET' );}{ features: { validation: 'zod' } }, at build time.