Beta You're reading the docs for Kubb v5, which is currently in beta. View the stable v4 docs
Skip to content

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.

adapterCustom.ts
typescript
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:

kubb.config.ts
typescript
import {  } from 'kubb'
import {  } from './adapterCustom.ts'
Cannot find module './adapterCustom.ts' or its corresponding type declarations.
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:

AdapterSource
typescript
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.

adapterStream.ts
typescript
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:

naming.ts
typescript
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.

shell
bun add -d @kubb/adapter-oas@beta
shell
pnpm add -D @kubb/adapter-oas@beta
shell
npm install --save-dev @kubb/adapter-oas@beta
shell
yarn add -D @kubb/adapter-oas@beta

Key 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.
kubb.config.ts
typescript
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:

adapterJsonSchema.ts
typescript
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:

kubb.config.ts
typescript
import {  } from 'kubb'
import {  } from './adapterJsonSchema.ts'
Cannot find module './adapterJsonSchema.ts' or its corresponding type declarations.
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:

Conversion pipeline
text
context → [rule.match → rule.convert] → node

The 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

adapterValidated.ts
typescript
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')
    }
  },
}))