Skip to content
How-to guides

Customize generated code

Change what a Kubb plugin generates without forking it: rename symbols and files with a resolver, rewrite schemas with a macro, or swap a printer.

You change what a plugin generates with one of these options, without forking it:

You want to changeUseApplies to
What a type, function, or file is called, or where a file landsresolverThe plugin you set it on
The schema itself, such as a field type, a name, or metadatamacrosEvery plugin you pass the macro to
The code printed for one schema type, such as dateprinter.nodes@kubb/plugin-ts, @kubb/plugin-zod, @kubb/plugin-faker

Macros run before the resolver, so when a macro changes a name, the generated names, paths, and files follow:

ASTMacrosGeneratorResolver: names and pathsPrinter: code per schematypeFiles
When an override uses this, write it as a regular method, not an arrow function.

Resolvers

A resolver names every symbol and file a plugin writes. Pass only the methods you replace to the plugin's resolver option. The rest keep their defaults.

MethodBuildsPlugin
name(name)Every identifierAll
file.baseName({ name, extname })A file name, extension includedAll
file.path({ baseName, output })The full file pathAll
query.keyName(node)A query key, such as getPetByIdQueryKey@kubb/plugin-react-query, @kubb/plugin-vue-query
response.status(node, statusCode)A response name, such as GetPetByIdStatus200@kubb/plugin-ts, @kubb/plugin-zod, @kubb/plugin-faker

Each plugin's options page lists its own namespaces. Resolver overrides has the signatures. resolver lists the shared members.

import { pluginTs, resolverTs } from '@kubb/plugin-ts'

pluginTs({
  resolver: {
    // Calls the preset to keep PascalCase: Pet becomes ApiPet
    name(name) {
      return `Api${resolverTs.name(name)}`
    },
  },
})
file.path replaces the plugin's output.path and group layout. The returned path resolves against the root output.path and must stay inside it. Moved files get no index.ts barrel.

Macros

A macro rewrites schema or operation nodes before any generator runs. Define one with defineMacro from kubb/kit:

FieldWhat it does
nameIdentifies the macro
A callback per node kind, such as schema or operationReturns a replacement node, or undefined to keep the node
matchSkips nodes the macro does not care about
enforce: 'pre' | 'post'Runs the macro before or after macros without enforce

ast.macro lists every callback.

import { ast, defineMacro } from 'kubb/kit'

export const macroIntegerToString = defineMacro({
  name: 'integer-to-string',
  schema(node) {
    if (node.type !== 'integer') return undefined
    // Copy only fields a string schema has: `min`, `max`, `primitive`, and `format` mean something else on an integer.
    const { name, title, description, nullable, optional, deprecated, readOnly, writeOnly } = node
    return ast.factory.createSchema({ type: 'string', name, title, description, nullable, optional, deprecated, readOnly, writeOnly })
  },
})

Register a macro

Pass macros through a plugin's macros option, or register them in your own plugin's setup with kubb.addMacro or kubb.setMacros. Macros run per plugin, so they never change the nodes another plugin sees.

plugin.ts
import { defineExtension, defineMacro } from 'kubb/kit'

const macroDropDescriptions = defineMacro({
  name: 'drop-descriptions',
  schema(node) {
    return 'description' in node && node.description ? { ...node, description: undefined } : undefined
  },
})

export const pluginRename = defineExtension({
  kind: 'plugin',
  meta: { name: 'plugin-rename' },
  setup(options, kubb) {
    kubb.addMacro(macroDropDescriptions)
    return options
  },
})

The diagram applies macroIntegerToString and macroDropDescriptions to a Pet node. Hover a line to see which macro changed it.

applyMacros(node, [integerToString, dropDescriptions])
SchemaNode in
{  kind: 'Schema', name: 'Pet',  description: 'A pet in the store',  properties: {    id: { type: 'integer',          format: 'int64' },    name: { type: 'string' },  },}
SchemaNode out
{  kind: 'Schema', name: 'Pet',  description: undefined,  properties: {    id: { type: 'string' },    name: { type: 'string' },  },}

integer-to-string returns a new node with type string, and the int64 format goes with it.

Keep macros pure. Return a new node instead of mutating the input, because every plugin shares the same AST nodes.

Rename a schema

Renaming only the declaration leaves every $ref on the old name, so imports point at a file the plugin no longer writes. Rename the declaration and set targetName on every ref in one macro, and register it on every plugin that reads the schema.

macroRenameSchema does both. Pass one shared macros array to each plugin:

import { ast, defineMacro } from 'kubb/kit'

export function macroRenameSchema({ from, to }: { from: string; to: string }) {
  return defineMacro({
    name: 'rename-schema',
    schema(node) {
      const refNode = ast.schema.narrow(node, 'ref')

      if (!refNode) {
        return node.name === from ? { ...node, name: to } : undefined
      }

      // A ref node can be the renamed schema's own declaration, a pointer to it, or both at once.
      const renamesDeclaration = refNode.name === from
      const renamesTarget = ast.ref.getName(refNode) === from
      if (!renamesDeclaration && !renamesTarget) return undefined

      return {
        ...refNode,
        ...(renamesDeclaration ? { name: to } : {}),
        ...(renamesTarget ? { targetName: to } : {}),
      }
    },
  })
}

Use the built-in macros

kubb/kit exports built-in macros for common normalizations. Combine them with your own:

kubb.config.ts
import { defineConfig } from 'kubb/config'
import { macroDedupeUnion, macroDiscriminatorEnum } from 'kubb/kit'
import { pluginTs } from '@kubb/plugin-ts'

export default defineConfig({
  input: './petStore.yaml',
  output: { path: './src/gen' },
  plugins: [
    pluginTs({
      macros: [macroDedupeUnion, macroDiscriminatorEnum({ propertyName: 'kind', values: ['cat', 'dog'] })],
    }),
  ],
})

Printers

printer.nodes replaces the handler that prints one schema type, such as date or object. A handler returns code as a string. To change the schema itself, use macros.

PluginA handler returnsInside a handler
@kubb/plugin-tsA TypeScript typethis.transform(node): a nested node's printed type
@kubb/plugin-zodA Zod expression, Zod Mini with mini: truethis.base(node): the default output. this.options.direction: see Zod encode and decode.
@kubb/plugin-fakerA Faker expressionthis.transform(node), this.options

Printer handlers and context has the full context.

import { pluginTs } from '@kubb/plugin-ts'

pluginTs({
  printer: {
    nodes: {
      // A JavaScript Date instead of string
      date() {
        return 'Date'
      },
    },
  },
})

TypeScript handlers

this.transform(node) returns a string, so a handler that passes it to ts.factory breaks. Build the type as a string.

Returning a TypeScript compiler node is deprecated and goes away in the next major. See Printer compiler nodes.

Zod encode and decode

this.options.direction is 'decode' for response schemas and 'encode' for request bodies and parameters. Type the handlers as PrinterZodNodes from @kubb/plugin-zod to read it. Encode custom types shows a direction-aware handler.

Less common cases

Printer compiler nodes

A TypeScript printer handler that passes this.transform(node) to ts.factory breaks, because this.transform(node) returns a string. Build the type as a string instead.

Returning a compiler node from typescript 5 or later, or @typescript/typescript6, still works with @kubb/parser-ts installed. It is deprecated and goes away in the next major.