Migration guide: v4 → v5
Kubb v5 splits responsibilities across adapters, plugins, parsers, and storage, so the upgrade is more than a version bump. The guide runs in upgrade order: requirements, packages, config, generated-code imports, then verify. Plugin-specific changes live on each per-extension page.
TIP
In a hurry? Run the upgrade prompt against your config to migrate most of it automatically, then read on to verify the result.
Before you start
System requirements
| Version | v4 | v5 |
|---|---|---|
| Node.js | ≥ 18 | ≥ 22 |
Update your CI pipelines, the engines field in package.json, and any Dockerfile FROM node lines. See Installation for the full setup.
Install the v5 packages
In v4, the code-generating plugins lived in kubb-labs/kubb. v5 moves them into kubb-labs/plugins. The npm package names stay the same, so you do not need to rename anything. The infrastructure packages that kubb wires in for you, @kubb/adapter-oas, @kubb/parser-ts, @kubb/parser-md, and @kubb/plugin-barrel, stay in kubb-labs/kubb.
bun add -d @kubb/plugin-ts@beta @kubb/plugin-zod@beta @kubb/plugin-axios@beta @kubb/plugin-fetch@beta \
@kubb/plugin-react-query@beta @kubb/plugin-vue-query@beta @kubb/plugin-swr@beta \
@kubb/plugin-faker@beta @kubb/plugin-msw@beta \
@kubb/plugin-mcp@beta @kubb/plugin-cypress@beta @kubb/plugin-redoc@betapnpm add -D @kubb/plugin-ts@beta @kubb/plugin-zod@beta @kubb/plugin-axios@beta @kubb/plugin-fetch@beta \
@kubb/plugin-react-query@beta @kubb/plugin-vue-query@beta @kubb/plugin-swr@beta \
@kubb/plugin-faker@beta @kubb/plugin-msw@beta \
@kubb/plugin-mcp@beta @kubb/plugin-cypress@beta @kubb/plugin-redoc@betanpm install -D @kubb/plugin-ts@beta @kubb/plugin-zod@beta @kubb/plugin-axios@beta @kubb/plugin-fetch@beta \
@kubb/plugin-react-query@beta @kubb/plugin-vue-query@beta @kubb/plugin-swr@beta \
@kubb/plugin-faker@beta @kubb/plugin-msw@beta \
@kubb/plugin-mcp@beta @kubb/plugin-cypress@beta @kubb/plugin-redoc@betayarn add -D @kubb/plugin-ts@beta @kubb/plugin-zod@beta @kubb/plugin-axios@beta @kubb/plugin-fetch@beta \
@kubb/plugin-react-query@beta @kubb/plugin-vue-query@beta @kubb/plugin-swr@beta \
@kubb/plugin-faker@beta @kubb/plugin-msw@beta \
@kubb/plugin-mcp@beta @kubb/plugin-cypress@beta @kubb/plugin-redoc@betaRemoved plugins
These plugins have no v5 equivalent. Remove them from your config and uninstall the packages.
| v4 package |
|---|
@kubb/plugin-solid-query |
@kubb/plugin-svelte-query |
Pick your upgrade path
Two ways to migrate kubb.config.ts: run the automated prompt, or follow the manual checklist. Either way, Migrate the config explains each change and verify confirms the result.
Automated: the upgrade prompt
Copy the prompt below, paste it into any LLM (Claude, ChatGPT, Gemini, …), and add your kubb.config.ts at the end.
Expand upgrade prompt
Migrate this kubb.config.ts from Kubb v4 to v5. Apply every rule, output the full updated file, and delete any import left unused after a removal.
Config:
1. Import `defineConfig` from `kubb/config`; if the config uses `memoryStorage`/`fsStorage`, import them from `kubb/kit`.
2. `input: { path }` / `input: { data }` → a single `input` value (path, URL, spec, or object).
3. `hooks.done` → `output.postGenerate` (array); drop top-level `hooks`. Move `output.storage` → top-level `storage`.
4. Remove `pluginOas()`; move its options to top-level `adapter: adapterOas({…})` from `@kubb/adapter-oas` (omit `adapter` if it had none — it defaults to `adapterOas()`). `serverIndex`/`serverVariables` → `server: { index, variables }`; `discriminator` `'strict'`→`'preserve'`, `'inherit'`→`'propagate'`.
5. Move onto `adapterOas` (one value each; if plugins disagreed, keep the value you want): `dateType`, `integerType`, `unknownType`, `emptySchemaType`, `enumSuffix`, `contentType`.
Output:
6. `output.barrelType` → `output.barrel`, at root and per plugin: `'named'`→`{type:'named'}`, `'all'`→`{type:'all'}`, `'propagate'`→`{type:'named',nested:true}`, `false`→`false`. v5 defaults `output.barrel` to `false`, so add `barrel:{type:'named'}` at the root if v4 relied on the old `'named'` default.
7. Leave `output.mode` alone: v5 reads it off the `output.path` extension exactly as v4 did. The root `output` takes no `mode`.
8. `output.format`/`output.lint` now default to `false` (were `'prettier'`/`'auto'`); keep any explicit value.
Remove `output.override` (root and per-plugin). Remove from every plugin: `generators`, `bundle`, `paramsType`, `pathParamsType`, `paramsCasing`, `dataReturnType`, `urlType`, `importPath`, `mapper` — and on plugin-swr also `mutation.paramsToTrigger`.
Plugins:
9. `transformers.name` → `resolver: { name(name) { … } }`, keeping your original logic and calling the plugin's exported resolver preset for the default casing (import it: `resolverTs` from `@kubb/plugin-ts`, `resolverZod` from `@kubb/plugin-zod`, …; `this.default.name` is the plain camelCase default). `transformers.schema` → `macros: [{ name: '<label>', schema(node) { … } }]` (name each macro).
10. plugin-zod: remove `version` and `typed`; drop `wrapOutput`, leaving a `// TODO: reintroduce wrapOutput via a printer override` comment; bump `zod` to `^4`.
11. Clients. `@kubb/plugin-client` is removed → `pluginAxios` (`@kubb/plugin-axios`, when old `client` was `'axios'`/unset) or `pluginFetch` (`@kubb/plugin-fetch`, when `'fetch'`), dropping `client`; `clientType: 'class'` → `sdk: {}` and `wrapper: { name }` → `sdk: { name }` (merge into one `sdk`). react-query/vue-query/swr/mcp no longer embed a client: if the v4 `client` was an object, register one `pluginAxios`/`pluginFetch` in `plugins[]` (shared by all consumers), move its `baseURL` there, and set `client: 'axios' | 'fetch'` on the query/mcp plugin (optional when only one client plugin is registered). On plugin-axios/plugin-fetch rename `parser` → `validator` (`false` | `'zod'` | `{ request, response }`; `'client'` → `false`); if a query plugin had `parser`, put `validator: 'zod'` on the client plugin. Leave plugin-msw `parser` (`'data'`/`'faker'`) unchanged.
12. Remove `pluginSolidQuery` (`@kubb/plugin-solid-query`) and `pluginSvelteQuery` (`@kubb/plugin-svelte-query`) — no v5 equivalent.
13. Keep every other option unchanged (group, include, exclude, override array, infinite, suspense, query, mutation, coercion, mini, seed, handlers, …).
Now migrate the following kubb.config.ts:Manual: the quick-path checklist
The highest-impact edits, in order. Each step links to its full explanation below.
- Bump Node to 22 and install the v5 packages at
@beta. - Change the import from
@kubb/coretokubb/config. - Give
inputa single value instead of{ path }/{ data }. - Remove
pluginOas()and move its options toadapter: adapterOas(...), along with the schema options that lived on each plugin. - Replace
pluginClientwithpluginAxiosorpluginFetch, and point query and MCP plugins at a client withclient: 'axios' | 'fetch'. - Convert
output.barrelTypetooutput.barrel, and addbarrel: { type: 'named' }if you relied on the old default. - Replace
transformers.namewithresolverandtransformers.schemawithmacros; removemapperandgenerators. - Move
hooks.donetooutput.postGenerateandstorageto the top level. - Bump the
zoddependency to^4(see Migration: @kubb/plugin-zod). - Opt back into formatting, linting, and a barrel if you want them, since all three now default to off.
Defaults that changed
The most dangerous changes are silent: a default flips, so the same config produces different output. Each row links to the full explanation and a before/after example.
| Option | v4 default | v5 default | Details |
|---|---|---|---|
output.format | 'prettier' | false | Formatting and linting are off by default |
output.lint | 'auto' | false | Formatting and linting are off by default |
output.barrel (barrelType in v4) | 'named' | false | output.barrelType → output.barrel |
group: { type: 'tag' } folder name | <tag>Controller | <tag> | Group folders drop the Controller suffix |
pluginReactQuery/pluginVueQuery hooks | true | false | @kubb/plugin-react-query, @kubb/plugin-vue-query |
pluginAxios/pluginFetch throwOnError | Errors returned on the result, never thrown | true | @kubb/plugin-client removed |
adapterOas integerType | 'number' | 'bigint' | Move schema options to the adapter |
pluginSwr mutation trigger shape (mutation.paramsToTrigger in v4) | Off, option opt-in | Always on, option removed | @kubb/plugin-swr |
Migrate the config
Every change to kubb.config.ts, grouped by the part of the config it touches. Work top to bottom.
Import defineConfig from kubb/config
Import defineConfig from the top-level kubb package. That package wires up the OpenAPI adapter, the TypeScript parsers, and the barrel plugin for you.
import { defineConfig } from '@kubb/core'import { defineConfig } from 'kubb/config'Give input a single value
The two-shape input object collapses into a single value: a file path, a URL, an inline spec, or a parsed object. Kubb works out which one it is.
| v4 (old) | v5 (new) |
|---|---|
input: { path: './api.yaml' } | input: './api.yaml' |
input: { data: spec } | input: spec |
export default defineConfig({
input: { path: './petstore.yaml' },
input: './petstore.yaml',
output: { path: './src/gen' },
})Leaving the wrapper in place fails the run with KUBB_LEGACY_INPUT. Earlier v5 betas read it as a parsed document, generated nothing, and still exited 0. If you migrated against one of those, re-run on a current version and check the file count.
Adopt the layered keys
v5 adds three top-level keys that replace behavior each plugin used to carry itself. Importing from kubb applies all three defaults, so set them only to change the defaults.
| Option | Package | Purpose | Default |
|---|---|---|---|
adapter | @kubb/adapter-oas | Parses the input spec into a universal AST. | adapterOas() |
parsers | @kubb/parser-ts, @kubb/parser-md | Converts AST nodes to .ts, .tsx, and .md files. | [parserTs(), parserTsx(), parserMd()] |
plugins (post) | @kubb/plugin-barrel | Post-processes output, like barrel files. | [pluginBarrel()] |
Move pluginOas options to the adapter
pluginOas() no longer belongs in plugins. Its options move to the top-level adapter key.
import { defineConfig } from '@kubb/core'
import { pluginOas } from '@kubb/plugin-oas'
import { pluginTs } from '@kubb/plugin-ts'
export default defineConfig({
input: './petstore.yaml',
output: { path: './src/gen' },
plugins: [
pluginOas({
validate: true,
serverIndex: 0,
serverVariables: { env: 'prod' },
discriminator: 'inherit',
}),
pluginTs(),
],
})import { defineConfig } from 'kubb/config'
import { adapterOas } from '@kubb/adapter-oas'
import { pluginTs } from '@kubb/plugin-ts'
export default defineConfig({
input: './petstore.yaml',
output: { path: './src/gen' },
adapter: adapterOas({
validate: true,
server: { index: 0, variables: { env: 'prod' } },
discriminator: 'propagate',
}),
plugins: [pluginTs()],
})The discriminator values were also renamed: 'strict' → 'preserve' (now the default) and 'inherit' → 'propagate'.
NOTE
Uninstall @kubb/plugin-oas. The adapter defaults to adapterOas() when importing from kubb, so the adapter: line is only required when you pass options.
Move schema options to the adapter
Several schema-level options that v4 accepted on each plugin now live once on adapterOas: dateType, integerType, unknownType, and emptySchemaType (from plugin-ts, plugin-zod, plugin-faker), enumSuffix (plugin-ts), and contentType (plugin-ts, plugin-zod, plugin-msw). Remove them from every plugin and set them once on the adapter.
IMPORTANT
The integerType default changed from 'number' to 'bigint', so OpenAPI int64 fields now map to bigint. Set integerType: 'number' on adapterOas to keep the old output.
The adapter page has the full table and before/after example.
Formatting and linting are off by default
output.format and output.lint both default to false in v5, so generation skips formatting and linting unless you opt in (v4 defaulted format to 'prettier' and lint to 'auto'). The accepted values are unchanged, but 'auto' now prefers the oxc tools first. See output.format and output.lint for the full value lists and detection order.
| Option | v4 default | v5 default |
|---|---|---|
output.format | 'prettier' | false |
output.lint | 'auto' | false |
output.barrelType → output.barrel
The string barrelType option becomes an object barrel option with a type field. At the plugin level, a nested flag replaces the old 'propagate' string.
v4 (old) output.barrelType | v5 (new) output.barrel |
|---|---|
'named' | { type: 'named' } |
'all' | { type: 'all' } |
'propagate' (plugin only) | { type: 'named', nested: true } |
false | false |
IMPORTANT
output.barrel also defaults to false, where v4's barrelType defaulted to 'named'. If your v4 config relied on that default, add output.barrel: { type: 'named' } to keep the barrel, or drop it since each plugin's output is already directly importable.
import { defineConfig } from '@kubb/core'
export default defineConfig({
input: './petstore.yaml',
output: { path: './src/gen', barrelType: 'named' },
})import { defineConfig } from 'kubb/config'
export default defineConfig({
input: './petstore.yaml',
output: { path: './src/gen', barrel: { type: 'named' } },
})The same change applies at the plugin level, on that plugin's own output:
import { defineConfig } from '@kubb/core'
import { pluginTs } from '@kubb/plugin-ts'
export default defineConfig({
input: './petstore.yaml',
output: { path: './src/gen' },
plugins: [pluginTs({ output: { barrelType: 'propagate' } })],
})import { defineConfig } from 'kubb/config'
import { pluginTs } from '@kubb/plugin-ts'
export default defineConfig({
input: './petstore.yaml',
output: { path: './src/gen' },
plugins: [pluginTs({ output: { path: 'types', barrel: { type: 'named', nested: true } } })],
})See @kubb/plugin-barrel for the full barrel option reference.
output.mode still follows output.path
Nothing to change here. Like v4, v5 reads the layout off the output.path extension: .ts means one file, anything else a folder.
output.mode | Layout |
|---|---|
'file' | One file for the whole plugin. |
'directory' | One file per operation or schema. |
Set mode when you want to override that, such as writing a folder called models.ts. group works with the inferred directory mode too, no mode needed alongside it. Pairing group with mode: 'file' stops the build with KUBB_INVALID_PLUGIN_OPTIONS, since a single file has nothing to group.
Group folders drop the Controller suffix
With group: { type: 'tag' }, each tag now writes to a folder named after the camelCased tag. v4 appended a Controller suffix (Requests for Cypress and MCP), so pet operations landed in petController/. v5 uses pet/ instead, and nothing else changes: your config stays the same, only the output folders do.
v4: src/gen/clients/petController/ → v5: src/gen/clients/pet/To keep the v4 layout, set group.name on the plugin:
import { defineConfig } from 'kubb/config'
import { pluginAxios } from '@kubb/plugin-axios'
export default defineConfig({
input: './petstore.yaml',
output: { path: './src/gen' },
plugins: [
pluginAxios({
output: { path: 'clients' },
group: { type: 'tag', name: ({ group }) => `${group}Controller` },
}),
],
})Replace output.override with storage
The output.override boolean is gone from the root and every plugin's output. It was meant to skip existing files, but v5 never read it, so remove it.
To keep certain files from being written, and to move storage from output.storage to the top-level storage key, supply a custom storage that no-ops setItem for the paths you protect:
import { defineConfig } from 'kubb/config'
import { fsStorage } from 'kubb/kit'
const base = fsStorage()
const protectedPaths = ['src/gen/.kubb/client.ts']
export default defineConfig({
input: './petStore.yaml',
output: { path: './src/gen', override: false },
output: { path: './src/gen' },
storage: {
...base,
async setItem(path, source) {
if (protectedPaths.some((p) => path.endsWith(p))) return
return base.setItem(path, source)
},
},
plugins: [],
})Import memoryStorage and fsStorage from kubb/kit, not @kubb/core.
Move hooks.done to output.postGenerate
The top-level hooks option is gone. Move its done commands into output.postGenerate. A single string becomes a one-item array, and { name, command } labels a step in the CLI output. The failure diagnostic is renamed KUBB_HOOK_FAILED → KUBB_POST_GENERATE_FAILED.
export default defineConfig({
input: './petstore.yaml',
output: {
path: './src/gen',
postGenerate: ['npm run typecheck'],
},
hooks: {
done: ['npm run typecheck'],
},
})--debug becomes reporters
The --debug flag and the debug value of --logLevel are gone. v5 renders a run through reporters instead, registered by the config reporters key (defineConfig registers the three built-ins for you) and selected at the CLI with --reporter, defaulting to cli. The file reporter replaces --debug, writing the same kind of log to .kubb/kubb[-<name>]-<timestamp>.log. See Reporters for what cli, json, and file each output.
kubb generate --debug
kubb generate --reporter fileThe kubb:debug hook and the createDebugger helper go away with the flag. See kubb generate for the full flag list and Diagnostics for the structured problem model the reporters render.
Update the shared plugin API
These changes apply to every plugin that used transformers in v4, plus the way query and MCP plugins reach a client.
transformers.name becomes resolver
A typed resolver replaces the single transformers.name(name, type) callback. Every plugin exposes a top-level name(name) method that sets identifier casing, so resolver: { name(name) { … } } is the shape for @kubb/plugin-ts, @kubb/plugin-zod, @kubb/plugin-axios, @kubb/plugin-fetch, @kubb/plugin-react-query, @kubb/plugin-vue-query, @kubb/plugin-swr, @kubb/plugin-msw, @kubb/plugin-faker, @kubb/plugin-cypress, and @kubb/plugin-mcp. Plugins that emit multiple symbols per operation add more resolver methods, listed on each plugin's reference page.
Call the plugin's exported preset (resolverTs.name, resolverZod.name, and so on) to keep the default casing. See Override a resolver for renaming and relocating generated files.
After porting a transformers.name callback, verify the generated identifiers: a v5 resolver can receive a differently-cased input than v4, so a custom transform may produce different names.
pluginTs({
transformers: {
name: (name) => `Api${name}`,
},
})import { pluginTs, resolverTs } from '@kubb/plugin-ts'
pluginTs({
resolver: {
name(name) {
return `Api${resolverTs.name(name)}`
},
},
})transformers.schema becomes macros
Schema-level transformations move to macros.
pluginZod({
transformers: {
schema: (schema) => ({ ...schema, description: undefined }),
},
})import { pluginZod } from '@kubb/plugin-zod'
pluginZod({
macros: [
{
name: 'strip-descriptions',
schema(node) {
return { ...node, description: undefined }
},
},
],
})mapper is removed
The mapper option is gone from every code-generating plugin. Override a node renderer with the printer option or rewrite nodes with macros instead. See Override a printer.
generators is removed
The generators plugin option is gone. To add custom output, build your own plugin. See Creating plugins.
Query and MCP plugins select a client
In v4, the client option on pluginReactQuery, pluginVueQuery, pluginSwr, and pluginMcp was an object that configured a bundled client (clientType, dataReturnType, baseURL, bundle, importPath). In v5 those plugins call a registered client plugin instead, so client is a single string that names it: 'axios' or 'fetch'. Register @kubb/plugin-axios or @kubb/plugin-fetch, set baseURL there, and point client at it. The client plugin returns the response body, so the generated code reads res.data and there is no dataReturnType. When exactly one client plugin is registered, Kubb auto-detects it and the string is optional. Each plugin's page shows the before/after for its own config.
Per-extension changes
Open the page for each extension you use. The table lists the headline change so you can tell at a glance whether a plugin needs work.
| Extension | Headline change |
|---|---|
@kubb/adapter-oas | Schema options moved here; integerType default is now 'bigint'; discriminator values renamed. |
@kubb/plugin-ts | Request params grouped into one *Options object; enum options restructured; mapper/paramsCasing removed. |
@kubb/plugin-zod | Zod v4 only; typed → inferred; wrapOutput/operations/mapper removed; response schema names gain a Status<code> segment. |
@kubb/plugin-faker | paramsCasing/mapper removed; createX is now generic over its return type. |
@kubb/plugin-client (removed) | Migrate to @kubb/plugin-axios or @kubb/plugin-fetch; clientType → sdk; parser → validator; throwOnError defaults to true. |
@kubb/plugin-react-query | client is a string selector; hooks defaults to false; params grouped; parser removed. |
@kubb/plugin-vue-query | Same as react-query, with hook arguments wrapped in MaybeRefOrGetter. |
@kubb/plugin-swr | client is a string selector; mutation.paramsToTrigger removed; requests key off shouldFetch. |
@kubb/plugin-msw | No config changes; handlers are typed against the request body and headers. |
@kubb/plugin-cypress | Request params grouped into one *Options object; dataReturnType removed; HTTP methods uppercased. |
@kubb/plugin-mcp | client is a string selector; now depends on plugin-ts and plugin-zod; paramsCasing removed. |
Generated output
v5 also changes what the generators emit, so update code that imports the generated files. Two changes apply to every generator:
- The banner (
/* Generated by Kubb */) is set byoutput.defaultBanneron the root config (default'simple'). Override it per plugin withoutput.bannerandoutput.footer, each taking a string or a per-file function. - Response status types now carry a
Status<code>suffix.
Operations that declare more than one requestBody content type now generate one type per content type plus a union alias, and the generated client takes a typed contentType argument. v4 used only the first content type, so single-content-type operations are unchanged.
The output changes specific to each generator live on its per-extension page.
Complete before/after example
A realistic multi-plugin config. Expand it to see every change from this guide applied together.
Show the full v4 → v5 config
import { defineConfig, memoryStorage } from '@kubb/core'
import { pluginOas } from '@kubb/plugin-oas'
import { pluginTs } from '@kubb/plugin-ts'
import { pluginZod } from '@kubb/plugin-zod'
import { pluginAxios } from '@kubb/plugin-axios'
import { pluginReactQuery } from '@kubb/plugin-react-query'
import { pluginFaker } from '@kubb/plugin-faker'
export default defineConfig({
input: './petstore.yaml',
output: {
path: './src/gen',
format: 'prettier',
storage: memoryStorage(), // → top-level `storage`
},
plugins: [
pluginOas({
// → top-level `adapter` with adapterOas()
validate: true,
serverIndex: 0,
discriminator: 'inherit',
}),
pluginTs({
output: { path: 'types' },
dateType: 'date', // → adapterOas
integerType: 'number', // → adapterOas
unknownType: 'unknown', // → adapterOas
enumSuffix: 'enum', // → adapterOas
mapper: {}, // removed (use printer or macros)
transformers: {
name: (name) => `Api${name}`,
},
}),
pluginZod({
output: { path: 'zod' },
version: '3', // removed (always Zod v4 in v5)
dateType: 'date', // → adapterOas
integerType: 'number', // → adapterOas
mapper: {}, // removed
}),
pluginAxios({
output: { path: 'clients' },
}),
pluginReactQuery({
output: { path: 'hooks' },
client: 'axios',
}),
pluginFaker({
output: { path: 'mocks' },
dateType: 'date', // → adapterOas
integerType: 'number', // → adapterOas
}),
],
})import { defineConfig } from 'kubb/config'
import { memoryStorage } from 'kubb/kit'
import { adapterOas } from '@kubb/adapter-oas'
import { pluginTs, resolverTs } from '@kubb/plugin-ts'
import { pluginZod } from '@kubb/plugin-zod'
import { pluginAxios } from '@kubb/plugin-axios'
import { pluginReactQuery } from '@kubb/plugin-react-query'
import { pluginFaker } from '@kubb/plugin-faker'
export default defineConfig({
input: './petstore.yaml',
output: {
path: './src/gen',
format: 'prettier',
barrel: { type: 'named' },
},
storage: memoryStorage(),
adapter: adapterOas({
validate: true,
server: { index: 0 },
discriminator: 'propagate',
dateType: 'date',
integerType: 'number',
unknownType: 'unknown',
enumSuffix: 'enum',
}),
plugins: [
pluginTs({
output: { path: 'types' },
resolver: {
name(name) {
return `Api${resolverTs.name(name)}`
},
},
}),
pluginZod({
output: { path: 'zod' },
}),
pluginAxios({
output: { path: 'clients' },
}),
pluginReactQuery({
output: { path: 'hooks' },
client: 'axios',
}),
pluginFaker({
output: { path: 'mocks' },
}),
],
})Every plugin keeps an extensionless output.path, which writes one file per operation. Give path a name ending in .ts to consolidate into a single file. The root output.barrel is set explicitly to keep v4's barrel.
Verify the upgrade
Once the config compiles, confirm the output:
- Run
kubb generateand review the diff against your previous output. - Type-check the generated code, and wire that check into
output.postGenerateso it runs on every generate. - Watch for a
KUBB_INVALID_PLUGIN_OPTIONSerror, which usually means a plugin'soutput.pathresolves to a file (an extension, or an explicitmode: 'file') whilegroupis also set. - Update imports of renamed symbols: response types now carry a
Status<code>suffix, and Zod inferred types end inType.
Performance
v5 generates code faster than v4, and the gap widens as the spec grows. The numbers below benchmark
@kubb/[email protected] against the v5 beta, where kubb and every plugin share the same
5.0.0-beta.109 version, median of five runs per configuration after a discarded warmup, both
versions writing to a fresh directory with formatting, linting, and barrel files disabled so the
comparison stays apples-to-apples.
Each timing covers a full kubb generate, from process start to the last file written.
faster generation on the OpenAI spec
Benchmark machine
| OS | Linux 6.18.5 |
|---|---|
| CPU | Intel(R) Xeon(R) Processor @ 2.10GHz |
| Cores | 4 |
| Memory | 15.7 GB |
| Node | v22.22.2 |
NOTE
Absolute milliseconds and megabytes are hardware-dependent. Treat the speedup percentages as the portable takeaway. Memory is peak resident set size for the whole Node process, so it counts the runtime baseline and not only the heap.
petStore.yaml, 21 operations
| Plugins | v4 time | v5 time | Speedup | v4 memory | v5 memory | Memory |
|---|---|---|---|---|---|---|
plugin-ts | 1,296 ms | 788 ms | +64% | 219.9 MB | 162.2 MB | +36% |
plugin-ts + plugin-axios | 1,366 ms | 777 ms | +76% | 221.5 MB | 164.2 MB | +35% |
plugin-ts + plugin-axios + plugin-zod + plugin-faker | 1,387 ms | 845 ms | +64% | 223.0 MB | 167.3 MB | +33% |
twitter.json, 80 operations, 374 KB
| Plugins | v4 time | v5 time | Speedup | v4 memory | v5 memory | Memory |
|---|---|---|---|---|---|---|
plugin-ts | 2,202 ms | 1,113 ms | +98% | 253.8 MB | 179.4 MB | +41% |
plugin-ts + plugin-axios | 2,680 ms | 1,160 ms | +131% | 255.9 MB | 180.1 MB | +42% |
plugin-ts + plugin-axios + plugin-zod + plugin-faker | 4,646 ms | 1,526 ms | +205% | 270.9 MB | 188.3 MB | +44% |
openai.json, 288 operations, 2.9 MB (openai/openai-openapi)
| Plugins | v4 time | v5 time | Speedup | v4 memory | v5 memory | Memory |
|---|---|---|---|---|---|---|
plugin-ts | 7,499 ms | 2,164 ms | +247% | 352.7 MB | 198.8 MB | +77% |
plugin-ts + plugin-axios | 8,731 ms | 2,311 ms | +278% | 408.8 MB | 201.8 MB | +103% |
plugin-ts + plugin-axios + plugin-zod + plugin-faker | 18,371 ms | 3,382 ms | +443% | 649.9 MB | 223.9 MB | +190% |
The OpenAI rows read the spec as JSON converted from the published YAML, since v4's yaml
dependency rejects the document with a mapping indentation error. v5 reads either form.
The gap widens on bigger specs. In v4, every plugin bootstraps its own pluginOas instance, so
parsing runs once per plugin and each copy stays in memory. In v5, adapterOas parses the spec
once and shares the result across all plugins. v5's input loading also gets cheaper as a spec
grows: it parses JSON with JSON.parse instead of always routing it through the YAML parser, and
once it confirms every $ref already points inside the document, it skips bundling instead of
walking the whole tree to find that out.
See also
- Adapters: how the OpenAPI input is parsed into the universal AST.
- Plugins: lifecycle, generators, and resolvers.
- Parsers: how AST nodes become source files.
- Override a resolver: rename symbols and files through the
resolveroption. - Override a printer: change the code a plugin emits for a schema type.
- Barrel files: barrel file generation with
@kubb/plugin-barrel. - Storage: switching between filesystem and in-memory storage.
@kubb/adapter-oas: every option that moved here from the plugins.- Plugin registry: the full list of v5 plugins.
- Recipes: copy-paste configurations for common scenarios.