@saunos/openapi-to-zod
v0.11.0
Published
Generate Zod schemas from an OpenAPI spec
Readme
@saunos/openapi-to-zod
Generate TypeScript source that exports Zod 4 schemas from an OpenAPI 3.x document or a standalone JSON Schema.
Install
npm install @saunos/openapi-to-zod
# or
bun add @saunos/openapi-to-zodCLI usage
Run directly with npx — no install needed:
npx openapi-to-zod https://example.com/openapi.json schemas.tsOr from a local file:
npx openapi-to-zod ./openapi.json ./generated/schemas.tsRun npx openapi-to-zod --help for the full option reference.
CLI options
| Flag | Description |
| ----------------------------------- | ------------------------------------------------------------------------------ |
| --mini | Emit zod/mini-compatible code (see Zod Mini) |
| --json-schema | Treat input as a standalone JSON Schema document (with $defs) |
| --use-date-codecs | Emit z.codec(...) for date / date-time formats, converting to Date |
| --alphabetical | Sort object property keys and enum values alphabetically |
| --no-strict | Collect all error-level diagnostics instead of throwing on the first |
| --no-strict-additional-properties | Don't append .strict() for additionalProperties: false |
| --no-default-non-nullable | Emit .optional() for non-required properties even when they have a default |
| --override pointer=expr | Replace the auto-generated expression at a JSON Pointer (repeatable) |
Library API
generateZodSourceFromOpenApi
import { generateZodSourceFromOpenApi } from '@saunos/openapi-to-zod';
const res = await fetch('https://example.com/openapi.json');
const openApiObject = await res.json();
const { code, diagnostics } = await generateZodSourceFromOpenApi(openApiObject, {
strict: true,
});
await Bun.write('./schemas.ts', code);generateZodSourceFromJsonSchema
For standalone JSON Schema documents that use $defs for named sub-schemas:
import { generateZodSourceFromJsonSchema } from '@saunos/openapi-to-zod';
import schema from './my-schema.json';
const { code, diagnostics } = generateZodSourceFromJsonSchema(schema);
await Bun.write('./schemas.ts', code);convertJsonSchemaToZod
For converting a single JSON Schema node to a Zod expression string (no full document, no $ref resolution):
import { convertJsonSchemaToZod } from '@saunos/openapi-to-zod';
const { expression } = convertJsonSchemaToZod({
type: 'object',
required: ['id', 'name'],
properties: {
id: { type: 'string', format: 'uuid' },
name: { type: 'string' },
age: { type: 'integer', minimum: 0 },
tags: { type: 'array', items: { type: 'string' } },
},
});
console.log(expression);
// z.object({
// id: z.uuid(),
// name: z.string(),
// age: z.int().min(0).optional(),
// tags: z.array(z.string()).optional(),
// })Note:
$refpointers to#/components/schemas/*are not resolved by this function — usegenerateZodSourceFromOpenApifor schemas with cross-references.
Zod Mini
Pass useZodMini: true (or --mini on the CLI) to emit tree-shakable zod/mini-compatible code. The functional API is used instead of method chains:
| Regular Zod | Zod Mini |
| ----------------------------- | --------------------------------------------------- |
| import { z } from 'zod' | import * as z from 'zod/mini' |
| z.string().min(1).max(50) | z.string().check(z.minLength(1), z.maxLength(50)) |
| z.number().min(0).max(5) | z.number().check(z.gte(0), z.lte(5)) |
| z.int().min(1) | z.int().check(z.gte(1)) |
| z.array(x).min(1) | z.array(x).check(z.minLength(1)) |
| schema.optional() | z.optional(schema) |
| schema.nullable() | z.nullable(schema) |
| schema.default(v) | z._default(schema, v) |
| z.object(shape).strict() | z.strictObject(shape) |
| z.object(shape).catchall(s) | z.catchall(z.object(shape), s) |
const { code } = await generateZodSourceFromOpenApi(spec, { useZodMini: true });Example
Given examples/bookstore.openapi.json, running:
npx openapi-to-zod ./examples/bookstore.openapi.json ./examples/bookstore.generated.tsProduces examples/bookstore.generated.ts:
// Auto-generated by @saunos/openapi-to-zod.
import { z } from 'zod';
const ErrorSchema = z.object({
code: z.string(),
message: z.string(),
});
const GenreSchema = z.enum(['fiction', 'non-fiction', 'science', 'history']);
const BookSchema = z.object({
id: z.uuid(),
title: z.string(),
author: z.string(),
get genre() {
return GenreSchema;
},
published_at: z.iso.datetime(),
rating: z.number().min(0).max(5).optional(),
});
const BookCreateSchema = z.object({
title: z.string(),
author: z.string(),
get genre() {
return GenreSchema;
},
published_at: z.union([z.iso.datetime(), z.null()]).optional(),
});
const BookPageSchema = z.object({
get items() {
return z.array(BookSchema);
},
total: z.int().min(0),
page: z.int().min(1),
size: z.int().min(1),
});
export const paths = {
'/books': {
get: {
path: z.object({}),
query: z.object({
genre: z.enum(['fiction', 'non-fiction', 'science', 'history']),
page: z.int().min(1),
size: z.int().min(1).max(100),
}),
responses: {
'200': BookPageSchema,
},
},
post: {
path: z.object({}),
query: z.object({}),
requestBody: {
'application/json': BookCreateSchema,
},
responses: {
'201': BookSchema,
},
},
},
'/books/{id}': {
get: {
path: z.object({
id: z.uuid(),
}),
query: z.object({}),
responses: {
'200': BookSchema,
'404': ErrorSchema,
},
},
},
} as const;
export const components = {
schemas: {
Book: BookSchema,
BookCreate: BookCreateSchema,
BookPage: BookPageSchema,
Error: ErrorSchema,
Genre: GenreSchema,
},
} as const;With --mini:
npx openapi-to-zod ./examples/bookstore.openapi.json ./examples/bookstore.generated.ts --mini// Auto-generated by @saunos/openapi-to-zod.
import * as z from 'zod/mini';
const ErrorSchema = z.object({
code: z.string(),
message: z.string(),
});
const GenreSchema = z.enum(['fiction', 'non-fiction', 'science', 'history']);
const BookSchema = z.object({
id: z.uuid(),
title: z.string(),
author: z.string(),
get genre() {
return GenreSchema;
},
published_at: z.iso.datetime(),
rating: z.optional(z.number().check(z.gte(0), z.lte(5))),
});
// ...
export const paths = {
'/books': {
get: {
query: z.object({
page: z.int().check(z.gte(1)),
size: z.int().check(z.gte(1), z.lte(100)),
}),
// ...
},
},
} as const;Generated exports
OpenAPI mode (generateZodSourceFromOpenApi)
Exports paths and components:
export const paths = {
'/resource/{id}': {
get: {
path: z.object({ ... }), // path parameters
query: z.object({ ... }), // query parameters
requestBody: { // optional — only present when defined
'application/json': ...,
},
responses: {
'200': ...,
},
},
},
} as const;
export const components = {
schemas: {
MyModel: MyModelSchema,
},
} as const;JSON Schema mode (generateZodSourceFromJsonSchema)
Exports schema (the root) and defs (named $defs):
export const schema = z.object({ ... });
export const defs = {
MyDef: MyDefSchema,
} as const;Overrides
Replace any auto-generated expression at a JSON Pointer with your own:
const { code } = await generateZodSourceFromOpenApi(spec, {
overrides: {
'#/components/schemas/Date': 'z.coerce.date()',
'#/components/schemas/User/properties/email': 'z.email().transform(v => v.toLowerCase())',
// Replace an entire group with a single expression
'#/paths/~1pets/get/queryParams': 'petsQuerySchema',
},
});Or via CLI:
npx openapi-to-zod api.json out.ts \
--override "#/components/schemas/Date=z.coerce.date()" \
--override "#/paths/~1pets/get/queryParams=petsQuerySchema"