04Reference
Generated code
The shape of models, operations, the client and the runtime.
Layout
Section titled “Layout”| Path | Content |
|---|---|
client.ts |
client (the default instance) and RequestOptions |
runtime/types.ts |
Transport contract, client and option types |
runtime/client.ts |
createClient() — URL building, auth, middleware, retries, error mapping |
runtime/middleware.ts |
retry() and refreshAuth() middleware (tree-shaken when unused) |
runtime/serialization.ts |
Parameter and body serialization |
runtime/errors.ts |
ApiError, RequestError, type guards |
runtime/signal.ts |
Timeout and signal composition |
runtime/fetch.ts or runtime/axios.ts |
The selected transport |
runtime/dates.ts |
reviveDates/encodeDates (only with types.dates: 'Date') |
models/<model>.ts |
One type per schema (plus const enum objects when enabled) |
models/<model>-input.ts |
Request-side variant of a model with readOnly/writeOnly properties (types.readWrite) |
dates/<model>.ts |
Where Date values sit in a model (only with types.dates: 'Date') |
webhooks/<webhook>.ts, callbacks/<operation>-<callback>.ts |
Payload, request, response and handler types (output.webhooks) |
<tag>/<operation>.ts |
One function per operation with its types and an error guard (isGetPetByIdApiError) |
react-query/<tag>/<operation>.ts, react-query/<tag>/keys.ts |
Hooks, option factories, key factories (data fetching); vue-query/, svelte-query/, solid-query/ or swr/ for the other libraries |
zod/<model>.ts |
Schemas (validation); valibot/ or arktype/ for the other libraries |
arktype/support.ts |
ArkType only: the reference and intersection helpers the schemas use |
index.ts, <hooks library>/index.ts, <schema library>/index.ts |
Barrels |
.swaggerz-manifest.json |
Generator ownership manifest |
Every file starts with /* eslint-disable */ and // @generated by swaggerz. Do not edit…. Output is deterministic: no timestamps or versions in files; the generator version is only recorded in the manifest.
| Schema | TypeScript |
|---|---|
string (any format) |
string; format: date/date-time become Date with types.dates: 'Date' |
integer, number |
number |
boolean / null |
boolean / null |
enum, const |
literal union, e.g. 'available' | 'sold' |
nullable, type: [T, 'null'], oneOf: [T, {type: 'null'}] |
T | null |
array / prefixItems |
T[] / [A, B, ...T[]] |
object |
{ name: T; optional?: U }; readonly for readOnly; property docs as JSDoc |
additionalProperties: S |
Record<string, S>, or an index signature that also accepts the declared property types |
oneOf/anyOf |
union; with a discriminator each variant is intersected with its tag (Circle & { kind: 'circle' }) |
allOf |
intersection; subtypes of a discriminated base get their tag (Pet & {…} & { petType: 'Cat' }) |
format: binary, Swagger file, binary media types |
Blob (request bodies also accept ArrayBuffer/ArrayBufferView) |
{} / true |
unknown |
Models whose names collide with globals used by generated code (Error, Blob, Record, …) are imported under an alias inside generated modules; their exported names are unchanged.
Request and response models
Section titled “Request and response models”With types.readWrite: 'split' (the default), a model that has readOnly or writeOnly properties, directly or through models it contains, gets a request-side variant named with naming.inputSuffix (abridged):
// models/account.ts — what the server returns: readOnly kept (as readonly), writeOnly removedexport type Account = { readonly id: string; name: string; createdAt: string };// models/account-input.ts — what you send: readOnly removed, writeOnly keptexport type AccountInput = { name: string; password: string };Request bodies and parameters use the …Input types, responses the plain ones. Models without such properties, and models only used in one direction, are not duplicated. Webhook and callback payloads use the inverted directions (the server sends the payload, your handler’s response goes back). 'merged' keeps a single type for both directions.
Operations
Section titled “Operations”export function addPet(params: AddPetParams, options?: RequestOptions): Promise<AddPetResponse>;-
No parameters and no body →
fn(options?); all optional →fn(params?, options?). -
paramscontains every parameter (by wire name or camelCase name) andbody. Optional members acceptundefined;undefinedandnullquery/header/cookie values are omitted. -
When a request body offers several media types,
paramsbecomes a union discriminated bycontentType. Omitting it selects the default (the first available of JSON, multipart, urlencoded, text, binary); every other media type has its own body type, listed in<Name>Bodies:await createAccount({ body: { name: 'Ada' } }); // application/jsonawait createAccount({ contentType: 'application/x-www-form-urlencoded', body: { name: 'Ada' } });await createAccount({contentType: 'application/xml',body: '<account><name>Ada</name></account>',});XML and other non-JSON text media types take the body already serialized (
string | Blob | ArrayBuffer | ArrayBufferView).Acceptis set to the preferred success media type. -
application/x-www-form-urlencodedbodies send arrays as repeated fields and nested objects as JSON, unless the property’sencodingdeclares astyleorexplode:deepObjectsendsmetadata[order_id]=6735, explodedformsends each property as its own field, and non-exploded styles join keys and values with,(or the style’s delimiter).multipart/form-databodies sendBlobs,Files and bytes as file parts (aFile’s name, otherwiseblob) and arrays as repeated parts. Object properties are JSON parts labeledapplication/json(OpenAPI’s default), and a property whoseencodingnames onecontentTypeis labeled with it; such parts are not files, so servers read them as fields rather than uploads. A wildcard request media type (*/*,text/*) is never sent asContent-Type: JSON bodies are sent asapplication/json, strings,Blobs and other native bodies with the transport’s default. -
Operations or paths with their own
serverssend requests to their first server (client.operationServers, on by default). The base URL passed per request still takes precedence;client.setConfig({ operationServers: false })turns it off at runtime. -
Response data is the union of 2xx and declared 3xx response bodies (JSON is parsed; text responses are strings; binary responses are
Blobs; bodiless responses such as204or304areundefined). If no 2xx response is declared,defaultis the success response. Undeclared 3xx statuses reject withApiError. -
Errors are typed by status: see Errors.
<Name>ErrorBodyis the union of the other declared responses’ bodies (undefinedfor bodiless ones,unknownwhen none are documented).
OpenAPI 3.2
Section titled “OpenAPI 3.2”-
QUERYand other methods.queryoperations sendQUERYwith their body; React Query treats them as queries (features.reactQuery.queryMethods). Methods fromadditionalOperationsare sent exactly as their key is spelled (COPY), and named like any other operation (copyDocument). The runtime’sHttpMethodtype accepts these methods. -
in: querystring. The parameter describes the whole query string and is one typed member ofparams. Form content (application/x-www-form-urlencoded, with itsencoding) is serialized with form rules (name=a+b&tags=x%2Cy); JSON content as JSON text and other media types as text, percent-encoded. API keys declaredin: queryare appended after a form query string. A JSON or text query string has no room for them, so security requirements that need them are skipped and the next alternative is used (or none, when there is no other); the generator warns about it (operation/querystring-credentials).await listPets({ filter: { name: 'Tom', tags: ['a', 'b'] } }); // GET /pets?name=Tom&tags=a%2Cbawait searchDocuments({ query: { text: 'q', limit: 5 } }); // GET /search?%7B%22text%22… -
style: cookiesends one cookie per array item or object property, separated by;, without percent-encoding (values must already be valid cookie values). -
Sequential media types described only by
itemSchema(application/jsonl,application/x-ndjson,application/json-seq,text/event-stream) are typed and read as text; the item models are generated so you can parse items yourself (operation/item-schema). PassparseAs: 'stream'to read the body as it arrives. -
Nested tags. With
output.groupBy: 'tag', an operation taggedcatswhose tag hasparent: petslives inpets/cats/, and its React Query keys start with['pets', 'cats'], so invalidatingpetsKeys.allalso invalidates the nested tag’s queries. -
discriminator.defaultMapping: the default schema is a union member without a discriminator value, for payloads without the property or with an unmapped value; schema validation accepts them.
With types.dates: 'Date', format: date and format: date-time strings are typed as Date everywhere: models, parameters, bodies, webhook payloads. For each model that contains dates, a dates/<model>.ts module describes where they are (AccountDates), and operations convert at the boundary:
- responses: parsed JSON is validated first (when validation is enabled, against the wire format:
z.iso.datetime({ offset: true })/z.iso.date()with Zod,v.isoTimestamp()/v.isoDate()with Valibot, ISO dates with ArkType), then strings becomeDateobjects (reviveDates); - requests: bodies and date parameters are encoded before validation and serialization (
date-time→ ISO string,date→YYYY-MM-DDin UTC).
Unions, recursion, arrays, maps and discriminated subtypes are followed. Encoding copies the body (your objects are not modified) and passes through values that are not Date objects; reviving leaves strings that are not valid dates as strings.
Webhooks and callbacks
Section titled “Webhooks and callbacks”With output.webhooks (on by default), every webhook (OpenAPI 3.1) and callback (3.0 and 3.1) gets type-only declarations for the side that receives it:
export type AccountClosedWebhookPayload = Account;export type AccountClosedWebhookRequest = { body: AccountClosedWebhookPayload; headers: { 'x-signature': string }; // header names lowercased, as Node and fetch expose them};export type AccountClosedWebhookResponse = { received?: boolean };export type AccountClosedWebhookHandler = ( request: AccountClosedWebhookRequest,) => AccountClosedWebhookResponse | Promise<AccountClosedWebhookResponse>;Callbacks are named after their operation and callback (CreateAccountAccountEventCallbackHandler in callbacks/create-account-account-event.ts). No server code is generated: use the types to annotate your own route handlers. They are exported from the main barrel.
Validation
Section titled “Validation”With features.validation, every model gets a schema module in the library’s directory, exporting <Model>Schema typed with the model it validates (unknown for models containing dates with types.dates: 'Date', whose schemas check the wire format):
| Library | Module | Schema type | Objects | References | Operation validation |
|---|---|---|---|---|---|
| Zod 4 | zod/pet.ts |
z.ZodType<Pet> |
z.looseObject, z.strictObject, .catchall() |
Getters in objects, z.lazy() elsewhere |
Schema.parse(data) |
| Valibot | valibot/pet.ts |
v.GenericSchema<Pet> |
v.looseObject, v.strictObject, v.objectWithRest; v.exactOptional |
v.lazy() with an explicitly typed getter |
v.parse(Schema, data) |
| ArkType | arktype/pet.ts |
Type<Pet> |
type({...}) (unknown keys kept), .onUndeclaredKey('reject'), rest() |
lazy() from arktype/support.ts, resolved during validation |
Schema.assert(data) |
All three keep unknown properties, enforce additionalProperties: false and typed additional properties, discriminators (including OpenAPI 3.2 default mappings), minLength/maxLength, numeric bounds and multipleOf, tuple items, and the assertions TypeScript cannot express (not, if/then/else, dependentSchemas, dependentRequired) as refinements (.refine(), v.check(), .narrow()). Formats and patterns are documentation only.
Where the libraries’ defaults differ from JSON Schema, the schemas follow JSON Schema and the generated types, identically in all three: string lengths count characters (not UTF-16 code units), a decimal multipleOf such as 0.01 tolerates floating-point rounding, integers may exceed 2^53 (int64 ids), optional properties named like Object.prototype members (constructor, toString) are only checked when the object has them, allOf reads like the TypeScript intersection (a member’s additionalProperties: false does not reject the properties other members declare), and patternProperties of an object without additionalProperties leave its other keys open. The client validates request bodies without their undefined properties, which JSON.stringify drops when sending. One exception in Valibot and ArkType: a closed model that contains itself through allOf reads like the intersection at its first level of nesting only; deeper levels refer to the closed schema, which rejects the other members’ properties.
ArkType resolves definitions eagerly and rejects contradictory intersections (string & number) when they are defined, which would make importing a module throw. The generated arktype/support.ts therefore provides lazy() (a reference resolved when data is validated, keeping the target’s error paths), allOf() (an intersection that falls back to checking every member when ArkType cannot combine them) and rest() (typed additional properties). ArkType’s types need TypeScript 5.4 or later.
Runtime contract
Section titled “Runtime contract”type Transport<TOptions> = (request: TransportRequest<TOptions>) => Promise<TransportResponse>;
interface TransportRequest<TOptions> { operationId: string; method: HttpMethod; url: string; headers: Headers; body: unknown; signal: AbortSignal | undefined; credentials: 'omit' | 'same-origin' | 'include' | undefined; parseAs: 'auto' | 'json' | 'text' | 'blob' | 'arrayBuffer' | 'stream'; transportOptions: TOptions | undefined;}
interface TransportResponse { status: number; statusText: string; headers: Headers; data: unknown; raw: unknown; error?: unknown;}A transport sends the already serialized request and reads the body of 2xx and 3xx responses according to parseAs; other responses (errors) are read by their Content-Type (auto: JSON is parsed, text is a string, anything else a Blob), so ApiError.body matches the declared error body even when the success response is binary or text. With parseAs: 'stream' every body is left unread. responseParseAs(parseAs, status) from runtime/serialization implements this rule for custom transports. A transport must not throw for HTTP error statuses. The axios transport requests every body as bytes (arraybuffer, or stream) and decodes it like the fetch transport, so binary responses are Blobs and malformed JSON in a successful response is a RequestError of kind parse with both. The client applies, in order: request body encoding (dates), request body validation, body serialization and timeout/signal composition once per call; then for every attempt default headers, Accept, header and cookie parameters, the first satisfiable security requirement, URL building, per-call headers, onRequest middleware, the transport (onError middleware may recover or retry) and onResponse middleware (may retry); then the status check (2xx and declared 3xx), response validation and response transformation (dates).
| Client API | |
|---|---|
client.setConfig(partial) / getConfig() |
baseUrl, transport, headers (object or function), auth, securitySchemes, timeout, credentials, transportOptions, operationServers |
client.use(middleware) |
Returns a function that removes it |
createClient(config) |
Additional instances; pass { client } to an operation |
Middleware
Section titled “Middleware”interface Middleware { onRequest?(context: MiddlewareContext): void | TransportRequest; // replace the request onResponse?(context: RetryableContext & { response }): void | TransportResponse | RetryDirective; onError?(context: RetryableContext & { error }): void | TransportResponse | RetryDirective; // recover}interface MiddlewareContext { request: TransportRequest; operation: OperationInfo; // operationId, method, path, security; one object per call attempt: number; // 1, then 2, 3… for retries client: Client; // e.g. to store a refreshed token with setConfig}interface RetryableContext extends MiddlewareContext { retry(): RetryDirective; // return it to send the request again}Middleware runs in registration order. Returning context.retry() from onResponse or onError discards the attempt and starts the next one: the request is built again from the current config (default headers and auth, so a refreshed token is used), every onRequest runs again with the next attempt number, and the response goes through every onResponse again. Middleware registered before the one that asked for the retry has already seen the discarded response. The call’s timeout and signal cover all attempts and the waits between them. operation is the same object for every attempt of a call, so middleware can keep per-call state in a WeakMap keyed by it.
Two middleware are built in:
retry(options) sends failed requests again with jittered exponential backoff: the n-th retry waits between half and all of min(maxDelay, delay × 2^(n−1)).
| Option | Default | |
|---|---|---|
attempts |
3 |
Attempts per call, including the first |
methods |
GET, HEAD, OPTIONS, PUT, DELETE, TRACE, QUERY |
Requests with an Idempotency-Key header are retried whatever their method |
statuses |
408, 429, 500, 502, 503, 504 |
Response statuses that are retried |
networkErrors |
true |
Retry when no response arrived (not for unreadable or invalid responses) |
delay, maxDelay |
300, 10000 |
Backoff in milliseconds |
respectRetryAfter |
true |
Wait as long as a Retry-After header (seconds or HTTP date) asks |
maxRetryAfter |
60000 |
Do not wait longer than this; the response is returned instead |
shouldRetry |
— | (context) => boolean, consulted last |
Request bodies that are streams are never sent twice. Aborting the call (or its timeout) ends a wait immediately: the call rejects with the abort error, or a RequestError of kind timeout.
refreshAuth(options) handles expired credentials: when a request of an operation with security requirements gets a 401 (statuses), it calls refresh() once, sends the request again and returns that response. Concurrent requests that fail while a refresh runs wait for it instead of refreshing again, and a request that was sent before a refresh finished is replayed with the new credentials without another refresh. Each call is replayed at most once, so a rejected new token does not loop.
// The token is stored in the client config as auth.bearerAuth.client.use(refreshAuth({ scheme: 'bearerAuth', refresh: () => api.renewToken() }));
// Or keep credentials in your own store and let `auth` functions read it.client.setConfig({ auth: { bearerAuth: () => session.token } });client.use( refreshAuth({ refresh: async () => { session.token = await api.renewToken(); }, }),);With scheme, only operations that use that scheme are refreshed; without it, every operation with a security requirement. Operations without security requirements, such as a login endpoint, never trigger a refresh. When refresh() throws, onFailure(error) is called and the call rejects with the original 401 ApiError.
auth values are strings or (async) functions per security scheme name: apiKey values are sent as declared (header, query or cookie); HTTP bearer, OAuth 2 and OpenID Connect values are sent as Authorization: Bearer <token>; HTTP basic values are username:password and are base64-encoded.
Errors
Section titled “Errors”| Error | When | Properties |
|---|---|---|
ApiError<Body, Status> |
status that is neither 2xx nor a declared 3xx | status, statusText, body, headers, response (raw), operationId, method, url, cause (axios: the AxiosError) |
RequestError |
kind: 'network' | 'timeout' | 'parse' | 'serialization' | 'validation' |
operationId, method, url, cause |
serialization covers every value of the request that cannot be sent: a missing path parameter, a body JSON.stringify rejects, or a header or cookie parameter that is not a valid header value (such as naïve ✓).
| original abort error | the caller’s AbortSignal aborted | — |
Each operation exports its errors typed by status:
export type GetPetByIdApiError = | ApiError<undefined, 400> // one member per documented status… | ApiError<ErrorModel, 404> | ApiError<unknown, Exclude<HttpStatus<1 | 3 | 4 | 5>, 400 | 404>>; // …and one for the restexport type GetPetByIdError = GetPetByIdApiError | RequestError;export function isGetPetByIdApiError(error: unknown): error is GetPetByIdApiError;HttpStatus<4> is the literal union of 400–499 (HttpStatus alone: 100–599), so a range response such as 4XX covers exactly the codes no explicit response claims, and default (or the last member, when there is no default) covers every status nothing else documents and that does not resolve. Because every member has a distinct status type, checking the status narrows the body:
try { await getPetById({ petId });} catch (error) { if (isGetPetByIdApiError(error)) { if (error.status === 404) error.body.message; // ErrorModel else if (error.status === 400) error.body; // undefined else error.body; // unknown: a status the specification does not document }}The guard checks instanceof ApiError and the operation (operationId and method), so errors of other operations are rejected. isApiError()/isRequestError() and instanceof work for any operation; an operation’s <Name>Error type (also the error of its React Query hooks) narrows the same way after instanceof ApiError. With responseStyle: 'full', success responses are discriminated the same way: ApiResponse<Pet, 200> | ApiResponse<undefined, 304>, and ranges such as 2XX become HttpStatus<2>. Every 2xx status resolves, so unless a 2XX range or a default response covers them, the 2xx statuses the specification does not declare are a member of their own with a body nothing describes (ApiResponse<unknown, Exclude<HttpStatus<2>, 200>>): check status before relying on data.
<Name>ErrorBody remains the union of the documented error bodies, for code that does not check the status.
JavaScript output
Section titled “JavaScript output”language: 'javascript' emits the same modules as ES2020 JavaScript:
- Types become
@typedefs; parameters and return values are annotated with@param/@returns; imports of types useimport('…')types, so no TypeScript syntax appears in.jsfiles. .d.tsdeclarations are emitted next to every module (unlessdeclarations: false; runtime declarations are always emitted).- JavaScript output is verified with TypeScript’s
checkJsunder strict settings, both with and without declarations.
Regeneration
Section titled “Regeneration”generate compares the new files with the output directory using the manifest:
| Situation | Action |
|---|---|
| File unchanged | Not written (mtime preserved) |
| Generated file changed | Rewritten |
| New file | Created |
| File no longer generated | Deleted (kept with a warning if edited by hand; kept entirely with --no-clean) |
| Non-generated file in the way | Error output/user-file (unless --force) |
| Directory in the way | Error output/user-file (also with --force): a directory where a file goes, or a file where a directory goes |
All new content is written to temporary files first, and every file to write or delete is checked to stay inside the output directory (symbolic links included) before anything changes; files are renamed into place only after every write succeeded, so a failure leaves the previous output intact. Temporary files that an interrupted run left behind (<file>.<pid>-<hex>.tmp, from a process that is no longer running) are removed by the next run.
Files are compared ignoring line endings, so a checkout that converted the generated files to CRLF (Git’s core.autocrlf) is neither rewritten nor reported by --check, and its stale files are not mistaken for hand edits. A file or directory whose name changes only in letter case (for example after changing naming.files) is renamed on case-insensitive file systems (macOS, Windows) instead of being deleted as stale.
Checking committed output in CI
Section titled “Checking committed output in CI”Projects that commit the generated client can fail a pull request when someone changes the specification or the config and forgets to regenerate:
swaggerz generate --check # writes nothing; exit 1 when the output differs from what would be generatedIt reports missing files, generated files that changed or were edited by hand, files that are no longer generated, and a manifest whose file list is out of date, then exits with 1 (0 when everything matches, 2 for invalid usage or configuration). --json puts the lists in the output (check.created, check.updated, check.deleted, check.manifest). The generator version recorded in the manifest is not compared, so upgrading swaggerz only fails the check when the generated code changes. Commit .swaggerz-manifest.json together with the generated files.
name: API clienton: [pull_request]jobs: generated-client: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - uses: actions/setup-node@v5 with: node-version: 24 - run: npm ci - run: npx swaggerz generate --check