Extension manifest
kubb-extension.json describes an extension package. It sits at the package root and ships on npm. The Kubb registry, kubb add, kubb list, and the Studio agent read it. Generation does not: at runtime Kubb reads the extension's meta.
The JSON Schema is https://kubb.dev/schemas/kubb-extension.json. Set it as $schema to get validation and completion in your editor.
{
"$schema": "https://kubb.dev/schemas/kubb-extension.json",
"kind": "plugin",
"name": "plugin-example",
"export": "pluginExample",
"displayName": "Example",
"description": "Writes a comment with the HTTP method and path of every operation.",
"category": "documentation",
"icon": { "light": "https://example.com/icon.svg" },
"tags": ["comments", "operations"],
"docs": "https://github.com/acme/kubb-plugin-example#readme",
"compatibility": { "kubb": ">=5.8.0", "node": ">=22" },
"options": [
{
"key": "prefix",
"path": ["prefix"],
"label": "Prefix",
"description": "Text written at the start of every comment.",
"type": "text"
}
]
}Fields
Readers ignore unknown top-level keys.
$schema
https://kubb.dev/schemas/kubb-extension.json. Only editors read it.kind
kubb add adds a plugin to the config and prints the code for an adapter or parser. Studio builds forms only for plugins.name
meta.name, such as plugin-zod, oas, or typescript. Kubb skips a manifest whose name is not shaped like an npm package name: lowercase letters, digits, ., _, ~, and -, with an optional @scope/.export
kubb.config.ts, such as pluginZod. It must be a JavaScript identifier. kubb add imports this name, and the Studio agent calls it.displayName
Zod.description
category
validation. The registry replaces it with the main category the reviewer picks from the category list.icon
media
tags
docs
compatibility
Semver ranges for Kubb (@kubb/core) and Node.js, such as { "kubb": ">=5.8.0", "node": ">=22" }. Only these two keys are allowed. Any other key makes the manifest invalid, so a typo such as nodejs cannot skip a check.
kubb add and kubb update check the ranges before they install. Kubb checks the same ranges at runtime through meta.compatibility, so import it from the manifest.
role
client provides the HTTP client runtime, like @kubb/plugin-fetch. consumer generates code that calls a client but ships none, like @kubb/plugin-react-query. Studio uses it to require a client plugin next to a consumer before it generates.options
Options
Each entry in options is one form field. It edits the value at path inside the options object users pass to the factory. Studio reads and writes every field with the same generic rules, so an extension needs no Studio code.
key
enumType.path
["enum", "type"] or ["output", "path"].label
Enum Type.description
type
default
choices
select. A choice with value: null means "leave unset". A default must equal one of the choice values.trueValue
boolean: the value written for true. Defaults to true.falseValue
boolean: the value written for false. Defaults to false.item
list: the template for each entry. Every "$item" string inside it is replaced by the entry. Without it, entries are plain strings.itemCase
list: upper-case each entry before writing it.JSONValue is any value that survives JSON.stringify: a string, number, boolean, null, array, or object.
Field types
A dropdown of choices. The chosen value is written as is, so it can be an object:
{
"key": "group",
"path": ["group"],
"label": "Group",
"type": "select",
"choices": [
{ "label": "none", "value": null },
{ "label": "by tag", "value": { "type": "tag" } },
{ "label": "by path", "value": { "type": "path" } }
]
}A switch. Set trueValue and falseValue when the option is not a plain boolean. This field writes suspense: {} when on and suspense: false when off:
{
"key": "suspense",
"path": ["suspense"],
"label": "Suspense",
"type": "boolean",
"default": false,
"trueValue": {},
"falseValue": false
}A comma-separated text input that writes an array. With item, each entry fills a template. This field turns pets, store into [{ "type": "tag", "pattern": "pets" }, { "type": "tag", "pattern": "store" }]:
{
"key": "excludeTags",
"path": ["exclude"],
"label": "Exclude Tags",
"type": "list",
"item": { "type": "tag", "pattern": "$item" }
}With "itemCase": "upper", get, post becomes ["GET", "POST"].
How Studio writes a value
text: trims the value. An empty string means unset.number: converts the value withNumber(). An empty value orNaNmeans unset.select: writes the chosenvalue.nullmeans unset.boolean: writestrueValue(defaulttrue) orfalseValue(defaultfalse).list: splits on,, trims each entry, drops empty entries, appliesitemCase, and fillsitem. An empty list means unset.- A value equal to
defaultis not written.["output", "mode"]is the exception, and Studio always writes it. - Unset removes the key at
pathfrom the config. Any other value is set atpath. - Writes deep-merge at
path, so["sdk", "mode"]and["sdk", "name"]share onesdkobject.
How Studio reads a value
Studio reads from the JSON copy of the options the agent reports, so a function inside an option never reaches it.
- It reads the value at
path. A missing value is unset. select: picks the choice whosevaluedeep-equals it. No match is unset.boolean: on when the value deep-equalstrueValue(defaulttrue), or whentrueValueis an object and the value is any object. Otherwise off.list: when the value is an array whose every entry matchesitem, with$itemstanding for any string, it joins the matched strings with,. Otherwise unset.textandnumber: the value when it is a string, number, or boolean.
A value Studio cannot show stays unset in the form, and the agent keeps the value in kubb.config.ts.
Validation
Kubb and the registry check a manifest at different points:
| Reader | Checks | On failure |
|---|---|---|
kubb add, kubb update, kubb list, the Studio agent | kind, name, export, displayName, and description are set and well formed, compatibility holds only kubb and node with string ranges, and options is an array. | The manifest is skipped. |
| The registry | The JSON Schema, unique option keys, choices on every select, and a select default that equals one of its choices. | The review pull request lists the problem, and the weekly bump keeps the previous pin. |
Import compatibility into meta
Keep the ranges in one place. Import only compatibility from the manifest into meta, so a bundler leaves the rest of the file, including options, out of your build:
import { definePlugin } from 'kubb/kit'
import { compatibility } from '../kubb-extension.json'
import { version } from '../package.json'
export const pluginExampleName = 'plugin-example'
export const pluginExample = definePlugin({
meta: { name: pluginExampleName, version, compatibility },
setup(options) {
return options
},
})Keep name as a typed constant. A JSON import widens it to string. Named imports from JSON need a bundler, such as tsdown, that inlines them. With plain tsc and "module": "NodeNext", write the same ranges in meta.compatibility instead.
See also
- Extensions:
metaand compatibility checks at runtime - Publish a plugin: ship the manifest and submit to the registry
- Extension registry: how the registry serves manifests