Skip to content
How-to guides

Transform schemas with macros

A macro is a named, composable transform over Kubb's AST. Macros rewrite schema and operation nodes before generators print code, and the same macro works across every adapter and every output target.

Use a macro to transform schema or operation nodes before generation. Macros can rename symbols, change field types, or remove metadata across output targets.

Import the macro engine through ast from kubb/kit. For callback signatures and context properties, see the AST reference.

Writing a macro

Define the node transformation:

macro.ts
import { ast } from 'kubb/kit'

const macroIntegerToString = ast.defineMacro({
  name: 'integer-to-string',
  schema(node) {
    return node.type === 'integer' ? { ...node, type: 'string' } : undefined
  },
})

The match predicate skips a macro for nodes it does not care about, and enforce places a macro before or after the unmarked ones.

enforce.ts
import { ast } from 'kubb/kit'

const macroUntagged = ast.defineMacro({
  name: 'untagged',
  enforce: 'post',
  match: (node) => node.kind === 'Operation',
  operation(node) {
    return node.tags?.length ? undefined : { ...node, tags: ['untagged'] }
  },
})

Using macros in a plugin

Pass macros through a plugin's macros option, or register them from setup with addMacro and setMacros. Macros run per plugin, so one plugin's macros never change the nodes another plugin sees.

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

const macroDropDescriptions = ast.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
  },
})

Together, the two macros on this page turn a Pet schema node into the node on the right. Hover a line to see what 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.

Macros run before resolver options are computed, so a renamed operationId or SchemaNode.name flows into resolveOptions, resolvePath, and resolveFile.

Keep macros pure. Build a new node and return it rather than mutating the input, since the AST is shared by reference.
Do not rename a schema by changing only the declaration's name. Every $ref to it still resolves to the old name, so imports and printed references point at a file the plugin no longer emits. Use macroRenameSchema, which renames the declaration and retargets the refs in one pass.

Use the built-in macros

kubb/kit exports presets for common normalizations. Compose them with your own through a plugin's macros option. See the built-in macros table for what each one does.

presets.ts
import { ast, macroSimplifyUnion, macroDiscriminatorEnum, macroRenameSchema } from 'kubb/kit'

const root = ast.factory.createInput({ schemas: [], operations: [] })
const next = ast.applyMacros(root, [
  macroSimplifyUnion,
  macroDiscriminatorEnum({ propertyName: 'kind', values: ['cat', 'dog'] }),
  macroRenameSchema({ from: 'Order', to: 'StoreOrder' }),
])

Export macros from a shared module to reuse them across plugins and projects.