Skip to content

Migration: @kubb/plugin-zod

Part of the v4 → v5 migration guide. See the full option reference in @kubb/plugin-zod.

Zod v3 no longer supported

The version option ('3' | '4') is removed. v5 always generates Zod v4 schemas.

Upgrade your zod dependency:

shell
bun add zod@^4
shell
pnpm add zod@^4
shell
npm install zod@^4
shell
yarn add zod@^4

Removed: mapper

Use macros or printer instead.

Renamed: transformers.name

resolver.resolveSchemaName replaces transformers.name.

Moved to adapterOas

dateType, integerType, unknownType, and emptySchemaType moved to adapterOas. See Migration: @kubb/adapter-oas.

New: mini

Generate the functional syntax of Zod Mini, which tree-shakes better. When mini: true, importPath defaults to 'zod/mini'.

kubb.config.ts
typescript
import {  } from 'kubb'
import {  } from '@kubb/plugin-zod'

export default ({
  : { : './petstore.yaml' },
  : { : './src/gen' },
  : [({ : true })],
})

Changed: inferred type names end with Type

With inferred: true, the z.infer<typeof schema> alias now carries a SchemaType suffix. petSchema exports PetSchemaType instead of PetSchema.

In v4 the schema value and its inferred type differed only by casing (petSchema and PetSchema). An all-uppercase name such as SUV, URL, or API produced the same identifier for both, so the barrel re-exported it twice and failed to compile with TS2300: Duplicate identifier. The Type suffix keeps the value and type apart no matter the casing.

zod/petSchema.ts
typescript
export const petSchema = z.object({
  name: z.string(),
  status: z.enum(['available', 'pending', 'sold']).optional(),
})

export type PetSchemaType = z.infer<typeof petSchema>
export type PetSchema = z.infer<typeof petSchema>

Update any imports that referenced the old name:

typescript
import type { PetSchemaType } from './gen/zod/petSchema.ts'
import type { PetSchema } from './gen/zod/petSchema.ts'

Generated output

Chained syntax instead of functional wrappers

v5 prefers the chained Zod 4 syntax. .optional() always sits at the end of the chain, right before .describe().

typescript
id: z.optional(z.int()),
shipDate: z.optional(z.iso.datetime()),
status: z.optional(z.enum(['placed', 'approved']).describe('Order Status')),
typescript
id: z.int().optional(),
shipDate: z.iso.datetime().optional(),
status: z.enum(['placed', 'approved']).optional().describe('Order Status'),

The functional form (z.optional(...)) is now reserved for mini: true output, which lives in its own configured output.path.

Self-referencing getters only for true cycles

v4 wrapped almost every nested ref in a getter. v5 only does so when the schema is truly circular, meaning it references itself or its parent.

diff
- get category() {
-   return categorySchema.optional()
- },
- get tags() {
-   return z.array(tagSchema).optional()
- },
+ category: categorySchema.optional(),
+ tags: z.array(tagSchema).optional(),
  get parent() {
    return z.array(petSchema).optional()
  },