Skip to content

04Reference

Generated code

The shape of models, operations, the client and the runtime.

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.

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 removed
export type Account = { readonly id: string; name: string; createdAt: string };
// models/account-input.ts — what you send: readOnly removed, writeOnly kept
export 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.

export function addPet(params: AddPetParams, options?: RequestOptions): Promise<AddPetResponse>;
  • No parameters and no body → fn(options?); all optional → fn(params?, options?).

  • params contains every parameter (by wire name or camelCase name) and body. Optional members accept undefined; undefined and null query/header/cookie values are omitted.

  • When a request body offers several media types, params becomes a union discriminated by contentType. 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/json
    await 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). Accept is set to the preferred success media type.

  • application/x-www-form-urlencoded bodies send arrays as repeated fields and nested objects as JSON, unless the property’s encoding declares a style or explode: deepObject sends metadata[order_id]=6735, exploded form sends each property as its own field, and non-exploded styles join keys and values with , (or the style’s delimiter). multipart/form-data bodies send Blobs, Files and bytes as file parts (a File’s name, otherwise blob) and arrays as repeated parts. Object properties are JSON parts labeled application/json (OpenAPI’s default), and a property whose encoding names one contentType is 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 as Content-Type: JSON bodies are sent as application/json, strings, Blobs and other native bodies with the transport’s default.

  • Operations or paths with their own servers send 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 as 204 or 304 are undefined). If no 2xx response is declared, default is the success response. Undeclared 3xx statuses reject with ApiError.

  • Errors are typed by status: see Errors. <Name>ErrorBody is the union of the other declared responses’ bodies (undefined for bodiless ones, unknown when none are documented).

  • QUERY and other methods. query operations send QUERY with their body; React Query treats them as queries (features.reactQuery.queryMethods). Methods from additionalOperations are sent exactly as their key is spelled (COPY), and named like any other operation (copyDocument). The runtime’s HttpMethod type accepts these methods.

  • in: querystring. The parameter describes the whole query string and is one typed member of params. Form content (application/x-www-form-urlencoded, with its encoding) 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 declared in: query are 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%2Cb
    await searchDocuments({ query: { text: 'q', limit: 5 } }); // GET /search?%7B%22text%22…
  • style: cookie sends 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). Pass parseAs: 'stream' to read the body as it arrives.

  • Nested tags. With output.groupBy: 'tag', an operation tagged cats whose tag has parent: pets lives in pets/cats/, and its React Query keys start with ['pets', 'cats'], so invalidating petsKeys.all also 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 become Date objects (reviveDates);
  • requests: bodies and date parameters are encoded before validation and serialization (date-time → ISO string, date → YYYY-MM-DD in 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.

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:

webhooks/account-closed.ts
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.

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.

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
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.

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 rest
export 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.

language: 'javascript' emits the same modules as ES2020 JavaScript:

  • Types become @typedefs; parameters and return values are annotated with @param/@returns; imports of types use import('…') types, so no TypeScript syntax appears in .js files.
  • .d.ts declarations are emitted next to every module (unless declarations: false; runtime declarations are always emitted).
  • JavaScript output is verified with TypeScript’s checkJs under strict settings, both with and without declarations.

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.

Projects that commit the generated client can fail a pull request when someone changes the specification or the config and forgets to regenerate:

Terminal window
swaggerz generate --check # writes nothing; exit 1 when the output differs from what would be generated

It 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.

.github/workflows/api-client.yml
name: API client
on: [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