# zod-openapi v5 📚 This guide explains the key changes between zod-openapi v4 and v5. You can migrate most of the way with our codemod: ```bash pnpmx codemod-zod-openapi-v5 'src/**/*.ts' npx codemod-zod-openapi-v5 'src/**/*.ts' ``` You can see the code here: [codemod-zod-openapi-v5](https://github.com/samchungy/codemod-zod-openapi-v5). ## 🔄 Differences from v4 ### 🛠️ Runtime Changes ✨ **Using native Zod metadata** Zod v4 introduces a native `.meta()` method for metadata, eliminating the need for runtime extensions: - No more runtime monkey-patching - Removed: `extendZodWithOpenApi(z)` and `import zod-openapi/extend` ```diff # Replace all schema definitions: - z.string().openapi({ ... }) + z.string().meta({ ... }) ``` > 💡 **Build-time optimization**: You can now declare `zod-openapi` as a dev dependency if you create your schemas at build time. ⚠️ **New requirements** 1. **Zod v4 (3.25.74) required** ```diff - import { z } from 'zod'; + import * as z from 'zod/v4'; ``` 2. **Node v20+ required** - Support for Node v18 has been dropped ### 📊 Schema Generation Changes The internal schema generation has been completely replaced with Zod's native `toJSONSchema()` method. This should result in an overall performance improvement. However, this will result in some changes to how some schemas are generated. Please raise a GitHub issue if you find any significant discrepancies. 🆕 **OpenAPI version requirements** - OpenAPI 3.1.0 is now the minimum required version - Support for OpenAPI 3.1.1 has been added ✨ **Lazy schemas supported natively** > Zod automatically creates lazy components, so you can use `z.lazy()` without any additional configuration. It is still recommended to provide an `id` for lazy schemas. 🔄 **Component references renamed: `ref` → `id`** ```diff - .openapi({ ref: 'MySchema' }) + .meta({ id: 'MySchema' }) ``` Wherever you previously used `ref` to create components within parameters, request objects and responses, has been replaced with `id` for consistency with Zod's syntax. 📝 **Input & Output Schema Generation** Zod's native `toJSONSchema()` handles object schemas differently than our previous implementation: - 📥 **Input schemas:** Allow additional properties by default (no `additionalProperties` specified) - 📤 **Output schemas:** Strip extra properties (`additionalProperties: false`) This better aligns with how Zod objects actually works, where input schemas are more permissive and output schemas are stricter and allows for [structured outputs](https://platform.openai.com/docs/guides/structured-outputs) for AI applications. As a result, when a schema containing a zod object with an ID is used in both request and response contexts, the library automatically generates two separate component schemas to properly represent the different behaviors. **How it works:** 1. Register a schema with an ID: ```ts const schema = z .object({ name: z.string(), age: z.number(), }) .meta({ id: 'Person', }); ``` 2. When used in both contexts, it automatically generates: ```ts components: { schemas: { Person: { type: 'object', properties: { name: { type: 'string' }, age: { type: 'number' }, }, }, PersonOutput: { type: 'object', properties: { name: { type: 'string' }, age: { type: 'number' }, }, additionalProperties: false, }, }, } ``` **Customization options:** - **Custom output name:** `.meta({ id: 'Person', outputId: 'CustomOutputName' })` - **Global suffix:** Set `outputIdSuffix` in `CreateDocumentOptions` to change from default "Output" In certain cases, the same component can be used for both `input` and `output` contexts. This happens when the schemas are identical - for example, with primitive types like `z.string().meta({ id: 'SimpleString'})`. The library performs an equality check between input and output representations to determine when a single schema can be safely reused for both contexts. For object schemas, you can explicitly control this behavior by using `z.looseObject()` (which allows additional properties) or `z.strictObject()` (which forbids them) instead of the standard `z.object()`. > **⚠️ Limitation:** If your schema contains dynamically created lazy components, they won't be reused between input and output schemas. ⚡ **Minor change:** Manual schema generation > Previously, unsupported Zod schemas without a `type` field would throw an error. Now, any schema with fields will be accepted. > > You can modify this behavior with the new `allowEmptySchema` option (see [New Options](#-new-options) below). ⚡ **Minor change:** `.extend()` behavior - No longer creates an embedded `allOf` referenced schema - This functionality would need to be contributed to the main Zod library if desired ### 🔧 Zod OpenAPI Options #### 🔁 Renamed Options 🔄 **`refType` → `unusedIO`** #### ❌ Removed Options ⚙️ **`effectType`** > Transforms are not introspectable. `effectType` was introduced to attempt to address this and to try and keep the transform locked to the same type as the input schema. > For transform operations, use Zod's native [`.overwrite()`](https://zod.dev/v4?id=overwrite) method, wrap your schema in a `.pipe()`, or declare a manual type. 🔄 **`unionOneOf`** 🔄 **`enforceDiscriminatedUnionComponents`** 🔄 **`defaultDateSchema`** > These options are replaced by the new override system (see below) #### 🛠️ New Override System **Direct property setting:** This is native to Zod v4 and allows you to set properties directly on the schema. ```ts z.date().meta({ type: 'string', format: 'date-time', }); ``` > **Note:** Zod's native type representation takes precedence over the fields set in the `meta()` method. > You can provide an `override` object or function to override this behavior. **Simple example:** ```ts z.string().meta({ override: { type: number, }, }); ``` **Advanced customization with functions:** ```ts // Example: achieving unionOneOf behavior z.union([z.string(), z.number()]).meta({ override: ({ jsonSchema }) => { jsonSchema.oneOf = jsonSchema.anyOf; delete jsonSchema.anyOf; }, }); ``` **Global overrides:** ```ts import { createDocument } from 'zod-openapi'; createDocument(document, { override: ({ jsonSchema, zodSchema, io }) => { const def = zodSchema._zod.def; if (def.type === 'date' && io === 'output') { jsonSchema.type = 'string'; jsonSchema.format = 'date-time'; } if (def.type === 'union') { jsonSchema.oneOf = jsonSchema.anyOf; delete jsonSchema.anyOf; } }, }); ``` > **Important:** Only an `override` function can be provided in `CreateDocumentOptions`. Schema-level `override` will always run after the global function. #### ✨ New Options 🔄 **`cycles`, `reused`** > These options are passed directly to Zod's `toJSONSchema()` method to control how cycles and reused schemas are handled. > > See [Zod's JSON Schema documentation](https://zod.dev/json-schema) for details. 🧩 **`allowEmptySchema`** > Controls when empty schemas can be created. This controls if zod-openapi will throw an error when an empty schema is encountered. > > - `{ [zodType]: true }` — Allow for both input and output > - `{ [zodType]: { input: true, output: true } }` — Allow for both input and output > - `{ [zodType]: { input: true } }` — Allow for input only > - `{ [zodType]: { output: true } }` — Allow for output only #### 🔁 Changed Options 🔄 **`refType` → `unusedIO`** > Renamed to better reflect its purpose: specifying how unused component schemas should be rendered. #### 📋 Create Schema Options 🔄 **`schemaType` → `io`** > Renamed to align with Zod's native terminology. 🔄 **`componentRefPath` → `schemaComponentRefPath`** > Renamed for clarity and consistency. 🔄 **`components` → `schemaComponents`** > Renamed for clarity and consistency. ### 🧩 New Components PathItems, SecuritySchemes, Links, and Examples can now be created as reusable components ### 🔌 zod-openapi/api Changes See [./api.md](./api.md) for detailed API changes in v5.