Skip to content

Macros

Macros are the second layer of @kubb/ast. The first layer is the AST itself, the node tree that adapters produce and generators read. Macros sit on top of it: a macro is a named, composable transform that rewrites those nodes before generators print code, so you can rename symbols, retype fields, strip metadata, or normalize shapes without forking an adapter or a generator. Because macros run on the shared AST, the same macro works across every input adapter (OpenAPI, AsyncAPI, JSON Schema) and every output target (TypeScript, Zod, and any printer a plugin supplies).

The engine (defineMacro, composeMacros, applyMacros, and the Macro type) lives on the @kubb/ast root, next to the node tree it transforms. The built-in macro presets live on the @kubb/ast/macros subpath, one per file.

Shape

A macro carries the per-kind callbacks of a visitor, plus a name, an optional enforce order, and an optional when gate.

ts
type Macro = {
  name: string
  enforce?: 'pre' | 'post'
  when?: (node: Node) => boolean
  schema?(node: SchemaNode, context): SchemaNode | null | undefined
  operation?(node: OperationNode, context): OperationNode | null | undefined
  // input, output, property, parameter, response
}

Each callback returns a replacement node, or undefined to leave the node untouched. A macro that changes nothing returns the original reference, so an unchanged tree is reused rather than rebuilt.

Writing a macro

defineMacro types a macro and reads as one construction site, the way definePlugin does for plugins.

macro.ts
typescript
import {  } from '@kubb/core'

const  = .({
  : 'integer-to-string',
  () {
    return . === 'integer' ? { ..., : 'string' } : 
  },
})

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

enforce.ts
typescript
import {  } from '@kubb/core'

const  = .({
  : 'untagged',
  : 'post',
  : () => . === 'Operation',
  () {
    return .?. ?  : { ..., : ['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.

compose.ts
typescript
import {  } from '@kubb/core'

const  = .({
  : 'dto',
  () {
    return . === 'object' ? { ..., : . ? `${.}Dto` : . } : 
  },
})

const  = .({
  : 'fetch-prefix',
  () {
    return { ..., : ..(/^get/, 'fetch') }
  },
})

const  = ..({ : [], : [] })
const  = .(, [, ])

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.

plugin.ts
typescript
import { ,  } from '@kubb/core'

const  = .({
  : 'drop-descriptions',
  () {
    return 'description' in  && . ? { ..., :  } : 
  },
})

export const  = (() => ({
  : 'plugin-rename',
  : {
    'kubb:plugin:setup'() {
      .()
    },
  },
}))

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

TIP

Keep macros pure. Build a new node and return it rather than mutating the input, since the AST is shared by reference.

Built-in macros

@kubb/ast/macros ships built-in macros for common schema normalizations that any adapter can apply. Import them like any macro and compose them with your own.

macroSimplifyUnion drops union members that a broader member already covers, such as a single-value string enum next to a plain string. macroDiscriminatorEnum rewrites a discriminator property into a string enum of its allowed values, and macroEnumName names an inline enum from the schema and property it belongs to. The last two read options, so you call them to build a macro.

presets.ts
typescript
import { ast } from '@kubb/core'
import { macroDiscriminatorEnum, macroSimplifyUnion } from '@kubb/ast/macros'

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

Sharing macros

A macro is a plain value, so you export it and import it wherever you need it. Group related macros in a module and reuse them across plugins and projects, the same way you reuse plugins.