@zodapi/codegen
v0.5.0
Published
Generate a zodapi contract (zod schemas + route defs) from an OpenAPI 3.1 document
Readme
@zodapi/codegen
Generates a zodapi contract — zod schemas plus plain
RouteDef route objects — from an OpenAPI 3.1 document. For backends not written in TypeScript:
point it at the spec your Python/Go/.NET/... framework emits and consume the result with
@zodapi/client.
zodapi-codegen openapi.json -o contract.tsor programmatically:
import { generateContract } from '@zodapi/codegen'
const source = generateContract(JSON.parse(await readFile('openapi.json', 'utf8')))What it generates
One file, importing only zod and @zodapi/core:
- an exported const per
components/schemasentry — component name = const name; recursive schemas use zod 4 shape getters - an exported const per operation, a plain object
satisfies RouteDef—operationIdbecomes the clientalias(no alias without one); the spec's declared responses are taken verbatim, nothing (like the zodapi400) is injected - a
routestuple ready forcreateClient(routes)from@zodapi/client - a
problemFlavorconst ('zodapi' | 'problem-details' | undefined, detected from the spec's error responses — zodapi'surn:zodapi:validationproblem type, or an ASP.NET-styleValidationProblemDetails) to feeddecodersFor(problemFlavor)when creating the client - with
exportTypes: true(CLI--export-types), atype <Name> = z.infer<typeof <Name>>alias alongside each component const (the zodios codegen convention).z.inferis the output side — usez.input<typeof <Name>>where the wire form differs (e.g. date codecs)
Documentation output
The spec's documentation (title, description, examples, deprecated, operation
summaries/tags) is emitted per docs: 'jsdoc' | 'meta' | 'none' (CLI --docs <mode>):
'jsdoc'(default): JSDoc comments on component consts, object properties, parameters, and route consts (summary/description body plus a@tagsline) — hover docs in the editor, zero runtime weight. No.meta()calls, no route doc fields.'meta': full fidelity — runtime.meta({...})calls,.meta({ id })component registration (so the schemas re-serialize as the same$refcomponents), andoperationId/summary/description/tagsfields on route objects. Use this when you regenerate a spec from the contract.'none': documentation is dropped entirely.defaultvalues, response descriptions, andaliasare structural and kept in every mode.
Query parameters typed array are declared with queryArray(item) from @zodapi/core, matching
the zodapi a[]= convention.
Output is unformatted; run your formatter over it.
Date conversion
By default ISO strings stay strings (z.iso.datetime() / z.iso.date()). Opt in to Date
conversion per format:
zodapi-codegen openapi.json -o contract.ts --dates-datetime --dates-date --dates-offsetgenerateContract(doc, { dates: { datetime: true, date: true, offset: true } })datetime/--dates-datetime:format: date-timefields become a bidirectionalz.codec(z.iso.datetime(), z.date(), ...)— responses parse toDate, requests encode back to the wire stringdate/--dates-date:format: datefields become a codec decoding toDateat UTC midnight and encoding back toYYYY-MM-DDoffset/--dates-offset: accept UTC offsets in date-time values (z.iso.datetime({ offset: true }))
The codecs are emitted once as shared isoDatetimeToDate / isoDateToDate consts; a field with
extra wire-side constraints or a default inlines the codec with those applied to its input side.
Because codecs change parsed values, @zodapi/client refuses calls whose validation mode would
skip a codec-bearing schema (see the client README); pair a dates contract with
validate: 'response' (the default) or 'both'. Codec-bearing params and query values always
take Date objects; add encodeRequests: true to pass them in request bodies too.
Fidelity
The converter covers the JSON Schema subset OpenAPI 3.1 uses: objects (required/optional,
additionalProperties as loose objects, catchalls, and records), arrays and tuples, unions
(oneOf is treated as anyOf), intersections (allOf), nullability in both encodings, enums and
consts, string formats (email, uuid, uri, date-time, ...), numeric/string/array
constraints, defaults, and description/title/examples metadata.
It is enforced by a round-trip test: a comprehensive hand-written contract is serialized to
OpenAPI, fed through the generator with docs: 'meta', and the document emitted from the
generated contract must deep-equal the original. ('jsdoc'/'none' contracts are deliberately
lossy — no component ids, no runtime metadata — so only 'meta' round-trips.)
Not covered
webhooks, refs outside #/components/schemas (component parameters/responses), response headers,
and OpenAPI 3.0 documents (3.1 only — 3.1 schemas are real JSON Schema, 3.0's nullable dialect is
not).
Install
pnpm add -D @zodapi/codegenThe generated file needs zod and @zodapi/core at runtime.
