Skip to content
Kit

Adapters

defineExtension with kind adapter builds an adapter that converts an input specification into the universal AST. Covers the Adapter interface, the built-in OpenAPI adapter, and writing your own.

An adapter converts an input specification into the shared AST.

For OpenAPI 2.0, 3.0, and 3.1 use the official @kubb/adapter-oas. Kubb picks it for you when you import defineConfig from the kubb package. Write a custom adapter only when you target a different specification such as AsyncAPI, GraphQL, JSON Schema, or gRPC.

defineExtension

defineExtension with kind: 'adapter' takes a definition object and returns a factory that types the adapter's options. setup turns the user's options into the resolved options, and build returns the adapter's document, parse, and validate for them. Without setup, the user's options are the resolved options.

A minimal adapter declares a name and returns an empty InputNode. An empty AST emits nothing, so fill schemas and operations from your spec next.

adapterCustom.ts
import { ast, defineExtension } from 'kubb/kit'
import type { AdapterFactoryOptions } from 'kubb/kit'

type AdapterCustom = AdapterFactoryOptions<'adapter-custom', { strict?: boolean }, { strict: boolean }>

export const adapterCustom = defineExtension<AdapterCustom>({
  kind: 'adapter',
  meta: { name: 'adapter-custom' },
  setup: (options) => ({ strict: options.strict ?? false }),
  build: () => ({
    document: null,
    async parse(_source) {
      return ast.factory.createInput({ schemas: [], operations: [] })
    },
    async validate() {
      // Throw or call ctx.error here when the spec is invalid.
    },
  }),
})

createAdapter (deprecated)

createAdapter is deprecated. Use defineExtension with kind: 'adapter' instead. The factory form below is removed in the next major.

createAdapter takes the same definition object as defineExtension, without kind. To move over, rename the call and add kind: 'adapter'. See Extensions.

Older adapters pass a function that returns the whole adapter, name included. To move over, copy name into meta.name, move the options defaults into setup, and return document, parse, and validate from build:

adapterCustom.ts
import { ast, createAdapter } from 'kubb/kit'
import type { AdapterFactoryOptions } from 'kubb/kit'

type AdapterCustom = AdapterFactoryOptions<'adapter-custom', { strict?: boolean }, { strict: boolean }>

export const adapterCustom = createAdapter<AdapterCustom>((options) => ({
  name: 'adapter-custom',
  options: { strict: options?.strict ?? false },
  document: null,
  async parse(_source) {
    return ast.factory.createInput({ schemas: [], operations: [] })
  },
  async validate() {
    // Throw or call ctx.error here when the spec is invalid.
  },
}))

Pass the adapter to your config:

kubb.config.ts
import { defineConfig } from 'kubb/config'
import { adapterCustom } from './adapterCustom.ts'

export default defineConfig({
  input: './my-spec.json',
  output: { path: './src/gen' },
  adapter: adapterCustom({ strict: true }),
  plugins: [],
})

Adapter anatomy

Every adapter the factory returns matches the Adapter interface from kubb/kit:

PropertyTypeRequiredPurpose
namestringYesUnique adapter identifier, filled from meta.name. Convention is adapter-<id>.
optionsTResolvedOptionsYesAdapter options after defaults are applied.
documentTDocument | nullYesThe raw parsed source document, for plugins that need direct access. null before parse().
parse(source: AdapterSource) => InputNode | Promise<InputNode>YesConvert the spec into the universal AST. The build driver consumes the returned InputNode directly.
validate(input: string, options?: { throwOnError?: boolean }) => Promise<void>YesValidate the document at a path or URL without running the full pipeline.

Cross-references need no adapter hook: every plugin resolves $ref imports through resolver.imports, which defaults each ref to its pointer's last segment. An adapter that renames a schema (for example to break a name collision) stamps targetName on every ref node pointing at it, so resolveRefName and those imports pick up the emitted name. Refs that keep their segment name need no stamp.

Throw from parse() with a clear, user-facing message when the input is invalid.

Adapter naming convention

Adapters share the layout of plugins, so getResolver, the registry, and the docs find them by inference:

SurfacePatternExample
npm package@<scope>/adapter-<name> or kubb-adapter-<name>@kubb/adapter-oas
Adapter runtime nameThe spec identifier (lowercase)'oas'
Factory exportadapter<Name> (camelCase)adapterOas
Name constantadapter<Name>NameadapterOasName
AdapterFactoryOptions aliasAdapter<Name> (PascalCase)AdapterOas

Export a satisfies-typed name constant so consumers can reuse the runtime name.