Adapters
An adapter turns your input specification into the universal AST. Every plugin reads that AST, so the adapter is the only part that knows the input format.
TIP
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.
Quick start
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.
import { , } from '@kubb/core'
import type { } from '@kubb/core'
type = <'adapter-custom', { ?: boolean }, { : boolean }>
export const = <>(() => ({
: 'adapter-custom',
: { : ?. ?? false },
: null,
async () {
return ..({ : [], : [] })
},
() {
return []
},
async () {
// Throw or call ctx.error here when the spec is invalid.
},
}))Wire it into your config with defineConfig from kubb and pass the adapter:
import { } from 'kubb'
import { } from './adapterCustom.ts'
export default ({
: { : './my-spec.json' },
: { : './src/gen' },
: ({ : true }),
: [],
})Anatomy
Every adapter returned from createAdapter matches the Adapter interface from @kubb/core:
| Property | Type | Required | Purpose |
|---|---|---|---|
name | string | Yes | Unique adapter identifier. Convention is adapter-<id>. |
options | TResolvedOptions | Yes | Adapter options after defaults are applied. |
document | TDocument | null | Yes | The raw parsed source document, for plugins that need direct access. null before parse(). |
parse | (source: AdapterSource) => InputNode | Promise<InputNode> | Yes | Convert the spec into the universal AST. The build driver consumes the returned InputNode directly. |
getImports | (node: SchemaNode, resolve: (name: string) => { name: string; path: string }) => Array<ImportNode> | Yes | Track cross-references so plugins emit correct imports. resolve receives the collision-corrected schema name and returns the { name, path } for the import. |
validate | (input: string, options?: { throwOnError?: boolean }) => Promise<void> | Yes | Validate the document at a path or URL without running the full pipeline. |
stream | (source: AdapterSource) => Promise<InputStreamNode> | No | Streaming variant of parse(). Returns schemas and operations as AsyncIterables. The OAS adapter uses this path for every spec. |
AdapterSource takes one of three shapes. Handle every form your users may pass:
type = { : 'path'; : string } | { : 'data'; : string | unknown } | { : 'paths'; : <string> }IMPORTANT
Throw from parse() with a clear, user-facing message when the input is invalid. Kubb surfaces the error verbatim.
Streaming
stream() returns an InputStreamNode whose schemas and operations are AsyncIterables instead of arrays. Each for await loop runs a fresh parse pass over the cached document, so plugins iterate independently and the runtime never holds every node in memory at once.
The build driver prefers stream() when an adapter implements it. For parse()-only adapters, the driver wraps the result in a reusable AsyncIterable so the rest of the pipeline stays stream-shaped.
import { , } from '@kubb/core'
import type { } from '@kubb/core'
type = <'adapter-stream', <string, never>>
async function* (): <.> {
// yield each parsed schema as soon as it is ready
}
async function* (): <.> {
// yield each parsed operation as soon as it is ready
}
export const = <>(() => ({
: 'adapter-stream',
: {},
: null,
async () {
throw new ('Use stream() instead. adapter-stream does not support eager parsing.')
},
async () {
return ..({
: true,
: (),
: (),
: { : 'Streamed spec', : [], : [] },
})
},
() {
return []
},
async () {
// Throw or call ctx.error here when the spec is invalid.
},
}))Build the result with ast.factory.createInput({ stream: true, schemas, operations, meta }) (see AST). The meta field is optional. Set it when you can, so plugins read title, version, and baseURL before the first node is yielded.
Naming convention
Adapters share the layout of plugins, so getResolver, the registry, and the docs find them by inference:
| Surface | Pattern | Example |
|---|---|---|
| npm package | @<scope>/adapter-<name> or kubb-adapter-<name> | @kubb/adapter-oas |
| Adapter runtime name | The spec identifier (lowercase) | 'oas' |
| Factory export | adapter<Name> (camelCase) | adapterOas |
| Name constant | adapter<Name>Name | adapterOasName |
AdapterFactoryOptions alias | Adapter<Name> (PascalCase) | AdapterOas |
Export the runtime name as a satisfies-typed constant so consumers reference it without typos:
import { , } from '@kubb/core'
import type { } from '@kubb/core'
export type = <'example', { ?: boolean }, { : boolean }, unknown>
export const = 'example' satisfies ['name']
export const = <>(() => ({
: ,
: { : . ?? false },
: null,
async () {
return ..()
},
() {
return []
},
async () {
// Throw or call ctx.error here when the spec is invalid.
},
}))Built-in adapters
@kubb/adapter-oas
Official adapter for OpenAPI 2.0 (Swagger), OpenAPI 3.0, and OpenAPI 3.1. Every official plugin is built against it. See the @kubb/adapter-oas reference for the full option list.
bun add -d @kubb/adapter-oas@betapnpm add -D @kubb/adapter-oas@betanpm install --save-dev @kubb/adapter-oas@betayarn add -D @kubb/adapter-oas@betaKey options:
| Option | Type | Default | Purpose |
|---|---|---|---|
validate | boolean | true | Run OpenAPI schema validation before parsing. |
dateType | 'date' | 'string' | 'stringOffset' | 'stringLocal' | 'date' | How format: date/date-time schemas are emitted in TypeScript. |
server | { index?: number; variables?: Record<string, string> } | — | Which servers[] entry to use as the base URL, and its variable overrides. |
import { } from 'kubb'
import { } from '@kubb/adapter-oas'
export default ({
: { : './petStore.yaml' },
: { : './src/gen' },
: ({ : true, : 'date', : { : 0 } }),
})NOTE
defineConfig from the kubb package uses adapterOas() when you omit adapter. Set adapter: only to configure adapterOas options or supply a different adapter.
Creating a custom adapter
Use createAdapter with AdapterFactoryOptions to model your input format. This JSON Schema adapter exposes the parsed document for plugins:
import { , } from '@kubb/core'
import type { } from '@kubb/core'
type = { : string; ?: <string, unknown> }
type = <'adapter-json-schema', { ?: boolean }, { : boolean }, >
export const = <>(() => {
let : | null = null
return {
: 'adapter-json-schema',
: { : ?. ?? false },
get () {
return
},
async (): <.> {
if (. !== 'path') {
throw new ('adapter-json-schema requires { type: "path" } input')
}
= { : 'https://json-schema.org/draft/2020-12/schema', : {} }
return ..({
: .(. ?? {}).(() => ..({ , : 'object', : [] })),
: [],
})
},
(, ) {
if (. === 'ref' && .) {
const = (.)
return [..({ : [.], : . })]
}
return []
},
async () {
// Throw or call ctx.error here when the spec is invalid.
},
}
})Register the adapter in kubb.config.ts:
import { } from 'kubb'
import { } from './adapterJsonSchema.ts'
export default ({
: { : './schema.json' },
: { : './src/gen' },
: ({ : true }),
: [],
})Schema dispatch and dialects
Turning a spec's schema objects into SchemaNodes is the heaviest part of an adapter. Most of that work is generic JSON Schema (oneOf/anyOf/allOf, enum, const, type, format, items, properties), so adapters follow one contract:
context → [rule.match → rule.convert] → nodeThe adapter derives a small context from each schema, then runs it through an ordered table of dispatch rules that map spec shapes onto AST nodes. Only a few decisions differ between specs. Those live behind a dialect, a single object the converter pipeline reads, so it never hard-codes OpenAPI assumptions:
| Decision | OpenAPI | AsyncAPI (example) |
|---|---|---|
| nullable | nullable: true, x-nullable, or type: ['…','null'] | type: ['…', 'null'] |
| discriminator | a discriminator object with a mapping | no discriminator object |
| binary | contentMediaType: 'application/octet-stream' | contentEncoding: 'binary' |
| optionality | a parent's required plus the schema's nullable set optional / nullish | same JSON Schema required + null |
@kubb/adapter-oas ships the OpenAPI dialect as its default. A new adapter such as @kubb/adapter-asyncapi reuses the same converters and dispatch table and supplies only its own dialect, so the spec-specific surface stays small. You test it by swapping that one object.
Examples
Validate before parsing
import { , } from '@kubb/core'
import type { } from '@kubb/core'
type = <'adapter-validated', <string, never>>
export const = <>(() => ({
: 'adapter-validated',
: {},
: null,
async () {
if (. !== 'path' || !..('.yaml')) {
throw new ('Expected a .yaml input file')
}
return ..({ : [], : [] })
},
() {
return []
},
async () {
if (!.('.yaml')) {
throw new ('Expected a .yaml input file')
}
},
}))