Skip to content

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
  • Responses (response, default true): the body of every successful response is parsed with its schema before the operation resolves.
  • Request bodies (request, default false): 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);
}
}

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.

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 and multipleOf;
  • additionalProperties: false (unknown properties are kept otherwise) and typed additional properties;
  • the assertions TypeScript cannot express: not, if/then/else, dependentSchemas and dependentRequired;
  • 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.

  • 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 doctor checks 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.

  • format and pattern are documentation only (except date and date-time with types.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 allOf reads like the intersection only at its first level of nesting.

What's generated

petstore.yaml
openapi: 3.1.0
info:
title: Petstore
version: '1.0'
servers:
- url: https://petstore.example.com/v1
paths:
/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: string

zod/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'
);
}
Generated by swaggerz 0.1.2 with { features: { validation: 'zod' } }, at build time.