
Product
Introducing Repository Access Permissions and Custom Roles
Socket now supports Custom Roles and Repository Access Permissions so organizations can control who can access specific repositories and actions.
@cerios/openapi-to-zod
Advanced tools
Generate Zod schemas from OpenAPI specifications. A TypeScript code generator that converts OpenAPI/Swagger YAML definitions into type-safe Zod validation schemas.
Transform OpenAPI YAML specifications into Zod v4 compliant schemas with full TypeScript support.
z.infer.optional() for optional properties instead of .partial()z.discriminatedUnion() for oneOf/anyOf with discriminators.describe() calls for better error messagesprefixItems support with .tuple() and .rest().extend() for objects (Zod v4), .and() for primitivesconst keyword support with z.literal()exclusiveMinimum/exclusiveMaximum with .gt()/.lt()uniqueItems validation with Set-based checking@deprecated JSDoc annotations for deprecated schemastitle and examples in JSDoc commentstype: ["string", "null"] supportnpm install @cerios/openapi-to-zod
npx @cerios/openapi-to-zod init
This interactive command will:
openapi-to-zod.config.ts or .json)npx @cerios/openapi-to-zod
The tool will auto-discover your config file and generate schemas.
outputTypesis the preferred config key. Deprecated alias:outputis still supported for backward compatibility. You must provide one ofoutputTypesoroutputper spec. If both are provided, they must have the same value.
Minimal:
import { defineConfig } from "@cerios/openapi-to-zod";
export default defineConfig({
specs: [
{
input: "openapi.yaml",
outputTypes: "src/schemas.ts",
},
],
});
With Commonly-Used Defaults:
import { defineConfig } from "@cerios/openapi-to-zod";
export default defineConfig({
defaults: {
mode: "strict", // Strictest validation
includeDescriptions: true, // Useful JSDoc comments
showStats: false, // Cleaner output
},
specs: [
{
input: "openapi.yaml",
outputTypes: "src/schemas.ts",
},
],
});
Multi-Spec with Custom Options:
import { defineConfig } from "@cerios/openapi-to-zod";
export default defineConfig({
defaults: {
mode: "strict",
includeDescriptions: true,
},
specs: [
{
name: "api-v1",
input: "specs/api-v1.yaml",
outputTypes: "src/schemas/v1.ts",
},
{
name: "api-v2",
input: "specs/api-v2.yaml",
outputTypes: "src/schemas/v2.ts",
mode: "normal", // Override default
prefix: "v2",
},
],
executionMode: "parallel", // Process specs in parallel (default)
});
openapi-to-zod.config.json:
{
"defaults": {
"mode": "strict",
"includeDescriptions": true
},
"specs": [
{
"input": "openapi.yaml",
"outputTypes": "src/schemas.ts"
}
]
}
openapi-to-zod [options]
Options:
-c, --config <path> Path to config file (optional if using auto-discovery)
-V, --version Output version number
-h, --help Display help
Commands:
init Initialize a new config file
Examples:
# Create config
$ openapi-to-zod init
# Generate (auto-discover config)
$ openapi-to-zod
# Generate with custom config path
$ openapi-to-zod --config custom.config.ts
| Option | Type | Description |
|---|---|---|
defaults | object | Global options applied to all specs (can be overridden per-spec) |
specs | array | Array of spec configurations (required, minimum 1) |
executionMode | "parallel" | "sequential" | How to process specs (default: "parallel") |
Per-Spec Options:
| Spec Option | Type | Description |
|---|---|---|
name | string | Optional identifier for logging |
input | string | Input OpenAPI YAML file path (required) |
outputTypes | string | Preferred output TypeScript file path (required unless deprecated output is set) |
outputZodSchemas | string | Separate output path for Zod schemas (recommended for circular references, see below) |
output | string | Deprecated alias for outputTypes; allowed for backward compatibility |
mode | "strict" | "normal" | "loose" | Validation mode for top-level schemas (default: "normal") |
emptyObjectBehavior | "strict" | "loose" | "record" | How to handle empty objects (default: "loose") |
includeDescriptions | boolean | Include JSDoc comments |
useDescribe | boolean | Add .describe() calls |
defaultNullable | boolean | Treat properties as nullable by default when not explicitly specified (default: false) |
schemaType | "all" | "request" | "response" | Schema filtering |
prefix | string | Prefix for schema names |
suffix | string | Suffix for schema names |
stripSchemaPrefix | string | Strip prefix from schema names before generating using glob patterns (e.g., "Company.Models." or "*.Models.") |
useOperationId | boolean | Use operationId for operation-derived query/header schema names when available (default: true) |
showStats | boolean | Include generation statistics |
request | object | Request-specific options (mode, includeDescriptions, useDescribe) |
response | object | Response-specific options (mode, includeDescriptions, useDescribe) |
operationFilters | object | Filter operations by tags, paths, methods, etc. (see below) |
If outputTypes and output are both set with different values, configuration validation fails.
Filter which operations to include/exclude during schema generation. Useful for generating separate schemas for different API subsets.
| Filter | Type | Description |
|---|---|---|
includeTags | string[] | Include only operations with these tags |
excludeTags | string[] | Exclude operations with these tags |
includePaths | string[] | Include only these paths (supports glob patterns like /users/**) |
excludePaths | string[] | Exclude these paths (supports glob patterns) |
includeMethods | string[] | Include only these HTTP methods (get, post, etc.) |
excludeMethods | string[] | Exclude these HTTP methods |
includeOperationIds | string[] | Include only these operationIds (supports glob patterns) |
excludeOperationIds | string[] | Exclude these operationIds (supports glob patterns) |
excludeDeprecated | boolean | Exclude deprecated operations |
Example:
export default defineConfig({
specs: [
{
input: "openapi.yaml",
outputTypes: "schemas.ts",
operationFilters: {
includeTags: ["public"], // Only public endpoints
excludeDeprecated: true, // Skip deprecated operations
excludePaths: ["/internal/**"], // Exclude internal paths
},
},
],
});
Parallel Mode (default):
Sequential Mode:
Both modes:
Example output:
Executing 3 spec(s) in parallel...
Processing [1/3] api-v1...
ā Successfully generated src/schemas/v1.ts
Processing [2/3] api-v2...
ā Successfully generated src/schemas/v2.ts
Processing [3/3] admin-api...
ā Failed to generate src/schemas/admin.ts: Invalid YAML syntax
==================================================
Batch Execution Summary
==================================================
Total specs: 3
Successful: 2
Failed: 1
Failed specs:
ā admin-api
Error: Failed to parse OpenAPI YAML file at specs/admin.yaml: Invalid YAML syntax
==================================================
import { OpenApiGenerator } from "@cerios/openapi-to-zod";
const generator = new OpenApiGenerator({
input: "path/to/openapi.yaml",
outputTypes: "path/to/schemas.ts",
mode: "normal", // 'strict' | 'normal' | 'loose'
includeDescriptions: true,
});
// Generate and write to file
generator.generate();
// Or generate as string
const code = generator.generateString();
Uses z.object() which allows additional properties:
const userSchema = z.object({
id: z.uuid(),
name: z.string(),
});
Uses z.strictObject() which rejects additional properties:
const userSchema = z.strictObject({
id: z.uuid(),
name: z.string(),
});
Uses z.looseObject() which explicitly allows additional properties:
const userSchema = z.looseObject({
id: z.uuid(),
name: z.string(),
});
When OpenAPI schemas define an object without any properties (e.g., type: object with no properties), the generator needs to decide how to represent it. The emptyObjectBehavior option controls this:
Uses z.looseObject({}) which allows any additional properties:
// OpenAPI: { type: object }
const metadataSchema = z.looseObject({});
// Accepts: {}, { foo: "bar" }, { any: "properties" }
Uses z.strictObject({}) which rejects any properties:
// OpenAPI: { type: object }
const emptySchema = z.strictObject({});
// Accepts: {}
// Rejects: { foo: "bar" }
Uses z.record(z.string(), z.unknown()) which treats it as an arbitrary key-value map:
// OpenAPI: { type: object }
const mapSchema = z.record(z.string(), z.unknown());
// Accepts: {}, { foo: "bar" }, { any: "properties" }
Note: The
modeoption controls how top-level schema definitions are wrapped, whileemptyObjectBehaviorcontrols how nested empty objects (properties without defined structure) are generated. These are independent settings.
components:
schemas:
UserStatusEnumOptions:
type: string
enum:
- active
- inactive
- pending
User:
type: object
required:
- id
- email
properties:
id:
type: string
format: uuid
minLength: 36
maxLength: 36
email:
type: string
format: email
maxLength: 255
name:
type: string
minLength: 1
maxLength: 100
age:
type: integer
minimum: 0
maximum: 150
status:
$ref: "#/components/schemas/UserStatusEnumOptions"
// Auto-generated by @cerios/openapi-to-zod
// Do not edit this file manually
import { z } from "zod";
// Enums
export enum UserStatusEnum {
Active = "active",
Inactive = "inactive",
Pending = "pending",
}
// Schemas
export const userStatusEnumOptionsSchema = z.enum(UserStatusEnum);
export const userSchema = z.object({
id: z.uuid().min(36).max(36),
email: z.email().max(255),
name: z.string().min(1).max(100).optional(),
age: z.number().int().gte(0).lte(150).optional(),
status: userStatusEnumOptionsSchema.optional(),
});
// Types
export type UserStatusEnumOptions = z.infer<typeof userStatusEnumOptionsSchema>;
export type User = z.infer<typeof userSchema>;
The generator supports all OpenAPI string formats with Zod v4:
| OpenAPI Format | Zod v4 Function |
|---|---|
uuid | z.uuid() |
email | z.email() |
url, uri | z.url() |
date | z.iso.date() |
date-time | z.iso.datetime() |
time | z.iso.time() |
duration | z.iso.duration() |
ipv4 | z.ipv4() |
ipv6 | z.ipv6() |
emoji | z.emoji() |
base64 | z.base64() |
base64url | z.base64url() |
nanoid | z.nanoid() |
cuid | z.cuid() |
cuid2 | z.cuid2() |
ulid | z.ulid() |
cidrv4 | z.cidrv4() |
cidrv6 | z.cidrv6() |
By default, the generator uses z.iso.datetime() for date-time format fields, which requires an ISO 8601 datetime string with a timezone suffix (e.g., 2026-01-07T14:30:00Z).
If your API returns date-times without the Z suffix (e.g., 2026-01-07T14:30:00), you can override this with a custom regex pattern:
import { defineConfig } from "@cerios/openapi-to-zod";
export default defineConfig({
defaults: {
// For date-times without Z suffix
customDateTimeFormatRegex: "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}$",
},
specs: [
{
input: "openapi.yaml",
outputTypes: "src/schemas.ts",
},
],
});
TypeScript Config - RegExp Literals:
In TypeScript config files, you can also use RegExp literals (which don't require double-escaping):
export default defineConfig({
defaults: {
// Use RegExp literal (single escaping)
customDateTimeFormatRegex: /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$/,
},
specs: [
{
input: "openapi.yaml",
outputTypes: "src/schemas.ts",
},
],
});
Common Custom Formats:
| Use Case | String Pattern (JSON/YAML) | RegExp Literal (TypeScript) |
|---|---|---|
No timezone suffix2026-01-07T14:30:00 | '^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}$' | /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$/ |
With milliseconds, no Z2026-01-07T14:30:00.123 | '^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}$' | /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}$/ |
Optional Z suffix2026-01-07T14:30:00 or2026-01-07T14:30:00Z | '^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}Z?$' | /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z?$/ |
With milliseconds + optional Z2026-01-07T14:30:00.123Z | '^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z?$' | /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z?$/ |
Generated Output:
When using a custom regex, the generator will produce:
// Instead of: z.iso.datetime()
// You get: z.string().regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$/)
Note: This option only affects date-time format fields. Other formats (like date, email, uuid) remain unchanged.
By default, the generator uses z.uuid() for uuid and guid format fields. You can change this with the uuidFormat option to use a specific UUID version or GUID validation:
import { defineConfig } from "@cerios/openapi-to-zod";
export default defineConfig({
defaults: {
uuidFormat: "uuidv4",
},
specs: [
{
input: "openapi.yaml",
outputTypes: "src/schemas.ts",
},
],
});
Available values:
| Value | Generated Output |
|---|---|
"uuid" (default) | z.uuid() |
"guid" | z.guid() |
"uuidv1" ā "uuidv8" | z.uuid({ version: "v1" }) etc. |
Note: Both format: "uuid" and format: "guid" in OpenAPI specs follow the configured uuidFormat setting.
Filter which operations are included in schema generation. This is useful when you want to generate schemas for only a subset of your API.
Example 1: Filter by tags
export default defineConfig({
specs: [
{
input: "openapi.yaml",
outputTypes: "public-schemas.ts",
operationFilters: {
includeTags: ["public", "users"], // Only include operations tagged with 'public' or 'users'
},
},
],
});
Example 2: Filter by paths
export default defineConfig({
specs: [
{
input: "openapi.yaml",
outputTypes: "v1-schemas.ts",
operationFilters: {
includePaths: ["/api/v1/**"], // Only v1 endpoints
excludePaths: ["/api/v1/admin/**"], // But exclude admin endpoints
},
},
],
});
Example 3: Exclude deprecated operations
export default defineConfig({
specs: [
{
input: "openapi.yaml",
outputTypes: "current-schemas.ts",
operationFilters: {
excludeDeprecated: true, // Skip all deprecated operations
},
},
],
});
Filtering Logic:
Statistics: When using operation filters, generation statistics will show how many operations were filtered out.
Generate separate schemas for requests and responses by filtering readOnly and writeOnly properties.
Example: Request schemas (exclude readOnly)
export default defineConfig({
specs: [
{
input: "openapi.yaml",
outputTypes: "request-schemas.ts",
schemaType: "request", // Excludes readOnly properties like 'id', 'createdAt'
},
],
});
Example: Response schemas (exclude writeOnly)
export default defineConfig({
specs: [
{
input: "openapi.yaml",
outputTypes: "response-schemas.ts",
schemaType: "response", // Excludes writeOnly properties like 'password'
},
],
});
Example: Context-specific validation
export default defineConfig({
specs: [
{
input: "openapi.yaml",
outputTypes: "schemas.ts",
request: {
mode: "strict", // Strict validation for incoming data
includeDescriptions: false,
},
response: {
mode: "loose", // Flexible validation for API responses
includeDescriptions: true,
},
},
],
});
OpenAPI Spec:
User:
type: object
properties:
id:
type: string
readOnly: true # Excluded in 'request' mode
email:
type: string
password:
type: string
writeOnly: true # Excluded in 'response' mode
createdAt:
type: string
format: date-time
readOnly: true # Excluded in 'request' mode
Generated Request Schema (schemaType: 'request'):
export const userSchema = z.object({
email: z.string(),
password: z.string(), // writeOnly included
// id and createdAt excluded (readOnly)
});
Generated Response Schema (schemaType: 'response'):
export const userSchema = z.object({
id: z.string(), // readOnly included
email: z.string(),
createdAt: z.string().datetime(), // readOnly included
// password excluded (writeOnly)
});
minLength and maxLength are automatically appliedpattern is converted to .regex()minimum becomes .gte()maximum becomes .lte()integer type uses .int()OpenAPI's nullable: true is converted to .nullable()
By default, properties are only nullable when explicitly marked with nullable: true (OpenAPI 3.0) or type: ["string", "null"] (OpenAPI 3.1).
However, many teams follow the industry de facto standard for OpenAPI 3.0.x where properties are assumed nullable unless explicitly constrained. You can enable this behavior with the defaultNullable option:
export default defineConfig({
specs: [
{
input: "openapi.yaml",
outputTypes: "schemas.ts",
defaultNullable: true, // Treat unspecified properties as nullable
},
],
});
Important: defaultNullable only applies to primitive property values within objects. It does NOT apply to:
$ref) - References preserve the nullability of the target schema; add explicit nullable: true if neededBehavior comparison:
| Schema Property | defaultNullable: false (default) | defaultNullable: true |
|---|---|---|
nullable: true | .nullable() | .nullable() |
nullable: false | No .nullable() | No .nullable() |
| No annotation (primitive) | No .nullable() | .nullable() |
No annotation ($ref) | No .nullable() | No .nullable() |
| No annotation (enum) | No .nullable() | No .nullable() |
| No annotation (const) | No .nullable() | No .nullable() |
Example:
components:
schemas:
Status:
type: string
enum: [active, inactive]
User:
type: object
properties:
id:
type: integer
name:
type: string
status:
$ref: "#/components/schemas/Status"
nullableStatus:
allOf:
- $ref: "#/components/schemas/Status"
nullable: true
With defaultNullable: false (default):
export const statusSchema = z.enum(["active", "inactive"]);
export const userSchema = z.object({
id: z.number().int(),
name: z.string(), // Not nullable (no annotation)
status: statusSchema, // Not nullable ($ref)
nullableStatus: statusSchema.nullable(), // Explicitly nullable
});
With defaultNullable: true:
export const statusSchema = z.enum(["active", "inactive"]);
export const userSchema = z.object({
id: z.number().int().nullable(), // Nullable (primitive)
name: z.string().nullable(), // Nullable (primitive)
status: statusSchema, // NOT nullable ($ref - must be explicit)
nullableStatus: statusSchema.nullable(), // Explicitly nullable
});
allOf ā .extend() for objects (Zod v4), .and() for primitivesoneOf, anyOf ā z.union() or z.discriminatedUnion()$ref ā Proper schema referencesEnums are generated based on their value types:
z.enum() for type-safe string unionsz.union([z.literal(n), ...]) for proper number typesz.boolean() for true/false valuesz.union([z.literal(...), ...]) for heterogeneous valuesExamples:
# String enum
Status:
type: string
enum: [active, inactive, pending]
# Integer enum
Priority:
type: integer
enum: [0, 1, 2, 3]
# Mixed enum
Value:
enum: [0, "none", 1, "some"]
Generated schemas:
// String enum ā z.enum()
export const statusSchema = z.enum(["active", "inactive", "pending"]);
export type Status = z.infer<typeof statusSchema>; // "active" | "inactive" | "pending"
// Integer enum ā z.union with z.literal
export const prioritySchema = z.union([z.literal(0), z.literal(1), z.literal(2), z.literal(3)]);
export type Priority = z.infer<typeof prioritySchema>; // 0 | 1 | 2 | 3
// Mixed enum ā z.union with z.literal
export const valueSchema = z.union([z.literal(0), z.literal("none"), z.literal(1), z.literal("some")]);
export type Value = z.infer<typeof valueSchema>; // 0 | "none" | 1 | "some"
Customize schema names with prefixes and suffixes:
// In your config file
export default defineConfig({
specs: [
{
input: "openapi.yaml",
outputTypes: "schemas.ts",
prefix: "api", // Output: apiUserSchema, apiProductSchema
suffix: "dto", // Output: userDtoSchema, productDtoSchema
},
],
});
This is useful when:
The stripSchemaPrefix option removes common prefixes from schema names in your OpenAPI spec before generating Zod schemas. This is particularly useful when your OpenAPI spec uses namespaced schema names (like .NET-generated specs with "Company.Models.User").
OpenAPI Spec with Namespaced Schemas:
components:
schemas:
Company.Models.User:
type: object
properties:
id:
type: string
name:
type: string
role:
$ref: "#/components/schemas/Company.Models.UserRole"
Company.Models.UserRole:
type: string
enum: [admin, user, guest]
Company.Models.Post:
type: object
properties:
id:
type: string
title:
type: string
author:
$ref: "#/components/schemas/Company.Models.User"
Without stripSchemaPrefix:
export const companyModelsUserRoleSchema = z.enum(["admin", "user", "guest"]);
export const companyModelsUserSchema = z.object({
id: z.string(),
name: z.string(),
role: companyModelsUserRoleSchema, // Long reference name
});
export const companyModelsPostSchema = z.object({
id: z.string(),
title: z.string(),
author: companyModelsUserSchema, // Long reference name
});
export type CompanyModelsUserRole = z.infer<typeof companyModelsUserRoleSchema>;
export type CompanyModelsUser = z.infer<typeof companyModelsUserSchema>;
export type CompanyModelsPost = z.infer<typeof companyModelsPostSchema>;
With stripSchemaPrefix: "Company.Models.":
export const userRoleSchema = z.enum(["admin", "user", "guest"]);
export const userSchema = z.object({
id: z.string(),
name: z.string(),
role: userRoleSchema, // Clean reference
});
export const postSchema = z.object({
id: z.string(),
title: z.string(),
author: userSchema, // Clean reference
});
export type UserRole = z.infer<typeof userRoleSchema>;
export type User = z.infer<typeof userSchema>;
export type Post = z.infer<typeof postSchema>;
export default defineConfig({
specs: [
{
input: "openapi.yaml",
outputTypes: "schemas.ts",
stripSchemaPrefix: "Company.Models.", // Strip this exact prefix
},
],
});
Use glob patterns to strip dynamic prefixes:
export default defineConfig({
specs: [
{
input: "openapi.yaml",
outputTypes: "schemas.ts",
// Strip any namespace prefix with wildcard
stripSchemaPrefix: "*.Models.",
},
],
});
Glob Pattern Syntax:
Glob patterns support powerful matching using minimatch:
* matches any characters within a single segment (stops at .)** matches any characters across multiple segments (crosses . boundaries)? matches a single character[abc] matches any character in the set{a,b} matches any of the alternatives!(pattern) matches anything except the pattern// Examples of glob patterns:
stripSchemaPrefix: "*.Models."; // Matches Company.Models., App.Models.
stripSchemaPrefix: "**.Models."; // Matches any depth: Company.Api.Models., App.V2.Models.
stripSchemaPrefix: "Company.{Models,Services}."; // Matches Company.Models. or Company.Services.
stripSchemaPrefix: "api_v[0-9]_"; // Matches api_v1_, api_v2_, etc.
stripSchemaPrefix: "v*.*."; // Matches v1.0., v2.1., etc.
stripSchemaPrefix: "!(Internal)*."; // Matches any prefix except those starting with Internal
Pattern 1: .NET Namespaces
{
stripSchemaPrefix: "Company.Models.";
}
// Company.Models.User ā User
// Company.Models.Post ā Post
Pattern 2: Multiple Namespaces with Wildcard
{
stripSchemaPrefix: "*.Models.";
}
// MyApp.Models.User ā User
// OtherApp.Models.User ā User
// Company.Models.Post ā Post
Pattern 3: Multiple Namespace Types
{
stripSchemaPrefix: "*.{Models,Services}.";
}
// App.Models.User ā User
// App.Services.UserService ā UserService
Pattern 4: Version Prefixes with Character Class
{
stripSchemaPrefix: "v[0-9].";
}
// v1.User ā User
// v2.Product ā Product
Pattern 5: Versioned Prefixes with Wildcards
{
stripSchemaPrefix: "api_v*_";
}
// api_v1_User ā User
// api_v2_Product ā Product
// api_v10_Comment ā Comment
stripSchemaPrefix is applied before prefix and suffix options:
export default defineConfig({
specs: [
{
input: "openapi.yaml",
outputTypes: "schemas.ts",
stripSchemaPrefix: "Company.Models.", // Applied first
prefix: "api", // Applied second
suffix: "dto", // Applied third
},
],
});
Result:
Company.Models.User ā User ā apiUserDtoSchemaCompany.Models.Post ā Post ā apiPostDtoSchemauserSchema instead of companyModelsUserSchemaUser type instead of CompanyModelsUserz.lazy()When your OpenAPI spec contains circular references (schemas that reference themselves or each other), Zod requires using z.lazy() for recursive types. However, this creates a TypeScript challenge:
// Combined mode - can cause TypeScript errors
export const nodeSchema = z.object({
id: z.string(),
parent: z.lazy(() => nodeSchema).optional(), // ā Type errors with circular inference
});
export type Node = z.infer<typeof nodeSchema>; // Circular type reference
Recommendation: Use separate type and schema files (outputZodSchemas) for specs with circular references:
import { defineConfig } from "@cerios/openapi-to-zod";
export default defineConfig({
specs: [
{
input: "openapi.yaml",
outputTypes: "src/generated/types.ts", // TypeScript types
outputZodSchemas: "src/generated/schemas.ts", // Zod schemas
},
],
});
This generates proper forward-declared types:
// types.ts
export interface Node {
id?: string;
parent?: Node;
}
// schemas.ts
import type { Node } from "./types";
export const nodeSchema: z.ZodType<Node> = z.object({
id: z.string().optional(),
parent: z.lazy(() => nodeSchema).optional(),
});
This approach also helps avoid "Type instantiation is excessively deep" errors (TS2589) with large schemas.
Statistics are included by default in generated files. Use showStats: false to disable:
// Generation Statistics:
// Total schemas: 42
// Circular references: 3
// Discriminated unions: 5
// With constraints: 18
// Generated at: 2025-12-07T06:21:47.634Z
Helpful for:
minLength ā .min(n)maxLength ā .max(n)pattern ā .regex(/pattern/)format ā Specific Zod validators (see Format Support section)minimum ā .gte(n) (inclusive)maximum ā .lte(n) (inclusive)exclusiveMinimum ā .gt(n) (OpenAPI 3.0 boolean or 3.1 number)exclusiveMaximum ā .lt(n) (OpenAPI 3.0 boolean or 3.1 number)multipleOf ā .multipleOf(n)integer type ā .int()Example:
Price:
type: number
minimum: 0
maximum: 10000
multipleOf: 0.01 # Enforces 2 decimal places
Generated:
export const priceSchema = z.number().gte(0).lte(10000).multipleOf(0.01);
OpenAPI 3.0 Style (boolean):
Percentage:
type: number
minimum: 0
maximum: 100
exclusiveMinimum: true
exclusiveMaximum: true
OpenAPI 3.1 Style (number):
Score:
type: number
exclusiveMinimum: 0
exclusiveMaximum: 100
Both generate:
export const percentageSchema = z.number().gt(0).lt(100);
minItems ā .min(n)maxItems ā .max(n)uniqueItems: true ā .refine() with Set-based validationExample:
UniqueTags:
type: array
items:
type: string
uniqueItems: true
minItems: 1
maxItems: 10
Generated:
export const uniqueTagsSchema = z
.array(z.string())
.min(1)
.max(10)
.refine(items => new Set(items).size === items.length, {
message: "Array items must be unique",
});
OpenAPI Spec Support: 3.1+
Use prefixItems for fixed-position array types (tuples):
Coordinates:
type: array
description: Geographic coordinates as [latitude, longitude]
prefixItems:
- type: number
minimum: -90
maximum: 90
- type: number
minimum: -180
maximum: 180
minItems: 2
maxItems: 2
Generated:
export const coordinatesSchema = z.tuple([z.number().gte(-90).lte(90), z.number().gte(-180).lte(180)]);
With Rest Items:
CommandArgs:
type: array
prefixItems:
- type: string # Command name
- type: string # Action
items:
type: string # Additional arguments
Generated:
export const commandArgsSchema = z.tuple([z.string(), z.string()]).rest(z.string());
required array ā Properties without .optional()additionalProperties: false ā .strict() (or implicit in strict mode)additionalProperties: true ā .catchall(z.unknown())additionalProperties: {schema} ā .catchall(schema)minProperties ā .refine() with property count validationmaxProperties ā .refine() with property count validationExample:
FlexibleMetadata:
type: object
minProperties: 1
maxProperties: 10
additionalProperties:
type: string
Generated:
export const flexibleMetadataSchema = z
.object({})
.catchall(z.string())
.refine(obj => Object.keys(obj).length >= 1 && Object.keys(obj).length <= 10, {
message: "Object must have between 1 and 10 properties",
});
Uses .extend() for objects (Zod v4 compliant - .merge() is deprecated), .and() for primitives:
Object Extending:
User:
allOf:
- $ref: "#/components/schemas/BaseEntity"
- $ref: "#/components/schemas/Timestamped"
- type: object
properties:
username:
type: string
required:
- username
Generated:
export const userSchema = baseEntitySchema.extend(timestampedSchema.shape).extend(
z.object({
username: z.string(),
}).shape
);
oneOf ā z.union() or z.discriminatedUnion() (if discriminator present)anyOf ā z.union() or z.discriminatedUnion() (if discriminator present)OpenAPI 3.0 Style:
NullableString:
type: string
nullable: true
OpenAPI 3.1 Style:
NullableString:
type: ["string", "null"]
Both generate:
export const nullableStringSchema = z.string().nullable();
Use const for exact value matching:
Environment:
type: string
const: "production"
Generated:
export const environmentSchema = z.literal("production");
Mark schemas or properties as deprecated:
OldUser:
type: object
deprecated: true
description: Legacy user schema
properties:
legacyId:
type: integer
deprecated: true
description: Old ID format, use uuid instead
Generated:
/** Legacy user schema @deprecated */
export const oldUserSchema = z.object({
/** Old ID format, use uuid instead @deprecated */
legacyId: z.number().int().optional(),
});
UserAccount:
title: User Account
description: Represents a user account in the system
type: object
Generated:
/** User Account Represents a user account in the system */
export const userAccountSchema = z.object({
/* ... */
});
StatusCode:
title: HTTP Status Code
type: string
enum: ["200", "201", "400", "404", "500"]
examples:
- "200"
- "404"
- "500"
Generated:
/** HTTP Status Code @example "200", "404", "500" */
export const statusCodeSchema = z.enum(["200", "201", "400", "404", "500"]);
| Feature | OpenAPI 3.0 | OpenAPI 3.1 | Zod Method |
|---|---|---|---|
| Basic types | ā | ā | z.string(), z.number(), etc. |
| String constraints | ā | ā | .min(), .max(), .regex() |
| Number constraints | ā | ā | .gte(), .lte(), .int() |
| Exclusive bounds (boolean) | ā | ā | .gt(), .lt() |
| Exclusive bounds (number) | ā | ā | .gt(), .lt() |
| multipleOf | ā | ā | .multipleOf() |
| Array constraints | ā | ā | .min(), .max() |
| uniqueItems | ā | ā | .refine() with Set |
| prefixItems (tuples) | ā | ā | z.tuple() |
| additionalProperties | ā | ā | .strict(), .catchall() |
| minProperties/maxProperties | ā | ā | .refine() |
| const | ā | ā | z.literal() |
| nullable (property) | ā | ā | .nullable() |
| nullable (type array) | ā | ā | .nullable() |
| allOf (objects) | ā | ā | .extend() |
| allOf (primitives) | ā | ā | .and() |
| oneOf/anyOf | ā | ā | z.union() |
| discriminators | ā | ā | z.discriminatedUnion() |
| deprecated | ā | ā | JSDoc @deprecated |
| title | ā | ā | JSDoc comment |
| examples | ā | ā | JSDoc @example |
| format | ā | ā | Specific Zod validators |
| readOnly/writeOnly | ā | ā | Schema filtering |
The generator provides clear, actionable error messages:
Error: Invalid schema 'User': Invalid reference at 'profile':
'#/components/schemas/NonExistentProfile' points to non-existent schema 'NonExistentProfile'
Error: Failed to parse OpenAPI YAML file at openapi.yaml:
Implicit keys need to be on a single line at line 12, column 9
All errors include:
Starting from v0.7.0, this package exports several utilities that can be used by other packages (like @cerios/openapi-to-zod-playwright):
LRUCache<K, V>A Least Recently Used (LRU) cache implementation for efficient caching.
import { LRUCache } from "@cerios/openapi-to-zod";
const cache = new LRUCache<string, ParsedSpec>(50);
cache.set("spec-key", parsedSpec);
const spec = cache.get("spec-key");
toPascalCase(str: string | number): stringConverts strings to PascalCase, handling kebab-case, snake_case, and special characters.
import { toPascalCase } from "@cerios/openapi-to-zod";
toPascalCase("my-api-client"); // => 'MyApiClient'
toPascalCase("user_name"); // => 'UserName'
escapeJSDoc(str: string): stringEscapes JSDoc comment terminators to prevent injection.
import { escapeJSDoc } from "@cerios/openapi-to-zod";
escapeJSDoc("Comment with */ terminator"); // => 'Comment with *\\/ terminator'
executeBatch<T>() and Generator InterfaceExecute batch processing with custom generators.
import { executeBatch, type Generator } from "@cerios/openapi-to-zod";
class MyGenerator implements Generator {
generate(): void {
// Your generation logic
}
}
await executeBatch(
specs,
"sequential", // or 'parallel'
spec => new MyGenerator(spec)
);
Shared utilities for configuration file validation:
import {
createTypeScriptLoader,
formatConfigValidationError,
type RequestResponseOptions,
type BaseOperationFilters,
} from "@cerios/openapi-to-zod";
// Create TypeScript config loader for cosmiconfig
const loader = createTypeScriptLoader();
// Format Zod validation errors
const errorMessage = formatConfigValidationError(zodError, filePath, configPath, [
"Additional note 1",
"Additional note 2",
]);
These utilities are marked with @shared tags in the source code and are covered by comprehensive tests.
OpenApiGeneratorMain class for generating Zod schemas from OpenAPI specifications.
import { OpenApiGenerator } from "@cerios/openapi-to-zod";
const generator = new OpenApiGenerator(options);
// Generate and write to file
generator.generate();
// Or generate as string
const code = generator.generateString();
interface OpenApiGeneratorOptions {
/**
* Input OpenAPI YAML/JSON file path
*/
input: string;
/**
* Output TypeScript file path
*/
outputTypes: string;
/**
* Object validation mode
* - 'strict': Uses z.strictObject() - no additional properties allowed
* - 'normal': Uses z.object() - additional properties allowed
* - 'loose': Uses z.looseObject() - explicitly allows additional properties
*/
mode?: "strict" | "normal" | "loose";
/**
* Whether to include descriptions as JSDoc comments
*/
includeDescriptions?: boolean;
/**
* Add custom prefix to schema names
*/
prefix?: string;
/**
* Add custom suffix to schema names
*/
suffix?: string;
/**
* Strip prefix from schema names using glob patterns
*/
stripSchemaPrefix?: string | string[];
/**
* Show generation statistics in output
*/
showStats?: boolean;
/**
* Schema filtering mode
*/
schemaType?: "all" | "request" | "response";
/**
* Operation filters for including/excluding operations
*/
operationFilters?: OperationFilters;
}
defineConfigType-safe helper for creating configuration files.
import { defineConfig } from "@cerios/openapi-to-zod";
export default defineConfig({
specs: [{ input: "api.yaml", outputTypes: "schemas.ts" }],
});
Comprehensive test suite with 364 passing tests covering:
MIT Ā© Ronald Veth - Cerios
Contributions are welcome! Please feel free to submit a Pull Request.
For issues and questions, please use the GitHub issues page.
FAQs
Generate Zod schemas from OpenAPI specifications. A TypeScript code generator that converts OpenAPI/Swagger YAML definitions into type-safe Zod validation schemas.
We found that @cerios/openapi-to-zod demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago.Ā It has 2 open source maintainers collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Product
Socket now supports Custom Roles and Repository Access Permissions so organizations can control who can access specific repositories and actions.

Product
Socket MCP now lets AI assistants review org alerts, investigate threats using the Socket threat feed, and inspect package files in addition to dependency scoring.

Product
Socket Firewall blocks malicious VS Code and Open VSX extensions before install, protecting developers from compromised editor marketplaces.