Transform schemas with macros
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. Built-in presets are named exports of kubb/kit.
For callback signatures and context properties, see the AST reference.
Writing a macro
Define the node transformation:
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.
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'] }
},
})
Composing macros
A plugin runs a list of macros. They apply in order, so a later macro sees the output of an earlier one. composeMacros folds a list into a single visitor, and applyMacros runs the list over a tree.
import { ast } from 'kubb/kit'
const macroDto = ast.defineMacro({
name: 'dto',
schema(node) {
return node.type === 'object' ? { ...node, name: node.name ? `${node.name}Dto` : node.name } : undefined
},
})
const macroFetchPrefix = ast.defineMacro({
name: 'fetch-prefix',
operation(node) {
return { ...node, operationId: node.operationId.replace(/^get/, 'fetch') }
},
})
const root = ast.factory.createInput({ schemas: [], operations: [] })
const next = ast.applyMacros(root, [macroDto, macroFetchPrefix])
Using macros in a plugin
Pass macros through a plugin's macros option, or register them from kubb:plugin:setup with addMacro and setMacros. Macros run per plugin, so one plugin's macros never change the nodes another plugin sees.
import { ast, definePlugin } 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 = definePlugin(() => ({
name: 'plugin-rename',
hooks: {
'kubb:plugin:setup'(ctx) {
ctx.addMacro(macroDropDescriptions)
},
},
}))
Macros run before resolver options are computed, so a renamed operationId or SchemaNode.name flows into resolveOptions, resolvePath, and resolveFile.
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.Built-in macros
Import built-in presets for common transformations:
macroSimplifyUniondrops union members that a broader member already covers, such as a multi-value string enum next to a plainstring. Single-value enums stay, since they narrow the type.macroDiscriminatorEnumrewrites a discriminator property into a string enum of its allowed values. It reads options, so you call it to build a macro.macroEnumNamenames an inline enum from the schema and property it belongs to. It reads options, so you call it to build a macro.macroRenameSchemarenames a schema consistently: it changes the declaration'snameand stampstargetNameon every ref that points at the old name, soresolveRefNameandresolver.importsemit the new name everywhere.
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' }),
])
Plugins that import another plugin's output compute names from the nodes they see, so register a rename on every plugin that touches the schema, for example by passing one shared macros array to each plugin's options.
Export macros from a shared module to reuse them across plugins and projects.