Generating OAS for HTTP APIs
If your route declares access: 'public' you must provide up-to-date OpenAPI specification for it. Docs for these routes get hosted on on our docs site and are used for client integrations. For example: our Elastic stack terraform provider.
Code-first API schemas must be designed carefully to produce clear OpenAPI 3.0 output. Prefer simple @kbn/config-schema types and keep request/response shapes narrow and explicit. For more information on how to design your API for OAS, see HTTP API Design.
Complex runtime-centric schemas can validate correctly but still generate confusing, lossy, or incomplete OAS. See <a href="#oas-compatibility-kbn-config-schema-types">types and patterns that do not map cleanly to OAS 3.0</a>.
Always make sure to preview the OAS you generated before merging it to main, run make help in /oas_docs for preview commands.
To get OAS generated for HTTP APIs you must use the following components:
- Core's
routerorrouter.versionedfor defining HTTP APIs provided via thecore.httpservice to all plugins @kbn/config-schemaor@kbn/zodrequest and response schemas
Kibana's core platform supports @kbn/config-schema as a first-class citizen for various schema purposes: configuration, saved objects, and HTTP API request/response bodies.
Developers can leverage @kbn/config-schema as a single-source of truth for runtime validation, TypeScript interfaces, and OpenAPI specification.
In kibana.dev.yml add the following configuration:
server.oas.enabled: true
Launch Kibana and send the following request:
curl -s -uelastic:changeme http://localhost:5601/api/oas\?pathStartsWith\=/api/foo
The value returned should contain the OpenAPI specification for your route and any other path's start with /api/foo.
Other useful query parameters for filtering are:
pluginId- get the OAS for a specific plugin, for example:@kbn/data-views-pluginaccess- filter for specific access levels:publicorinternalare supported
<a id="oas-compatibility-kbn-config-schema-types"></a>
Use this section as a practical checklist when authoring public APIs.
@kbn/config-schema type/pattern |
Why this is problematic for OAS 3.0 | Preferred alternative |
|---|---|---|
schema.byteSize() / schema.duration() |
Parses to runtime-specific value types (ByteSizeValue, moment.Duration) that are not standard JSON schema primitives for OpenAPI consumers. |
Use schema.string() (human-readable units) or schema.number() (normalized base unit), and document units in meta.description (for example: "duration in milliseconds", "size in bytes"). |
schema.buffer() / schema.stream() |
Binary and stream runtime objects do not map naturally to standard JSON request/response bodies. | Model payloads as JSON-friendly primitives/objects. For binary transport, document media type and use explicit OpenAPI request/response content definitions. |
schema.any() |
Escape hatch that produces little to no useful contract in generated OAS. | Use a small explicit schema.object({...}), schema.recordOf(...), or a constrained union with documented fields. |
schema.mapOf() / schema.recordOf() when key shape matters |
OAS generally represents these as type: object with additionalProperties, which does not clearly communicate all key constraints to clients. |
If keys are known, model explicit object properties. If keys are dynamic, keep values simple and document key format in descriptions. |
schema.oneOf() for object variants (especially nested oneOf) |
Unions without a clear discriminator are harder for humans/tools to understand. Nested forms like schema.oneOf([schema.oneOf([a, b]), schema.oneOf([c, d])]) are especially hard to read and lead to poor validation errors. |
Prefer schema.discriminatedUnion('type', [...]) with a stable discriminator and flat variants. |
schema.conditional(), schema.contextRef(), schema.siblingRef() |
Behavior depends on runtime context, which is difficult to encode as a stable, portable OAS contract. | Prefer explicit route versions or explicit discriminator/object shapes so behavior is statically visible in the contract. |
// In server/schemas/v1.ts
import { schema, TypeOf } from '@kbn/config-schema';
export const fooResource = schema.object({
name: schema.string({
meta: { description: 'A unique identifier for...' },
}),
// ...and any other fields you may need
});
export type FooResource = TypeOf<typeof fooResource>;
// In common/foo/v1.ts
export type { FooResource } from '../server/schemas/v1';
// In common/index.ts expose this as the "latest" schema shape
export type { FooResource } from './latest';
export * as fooResourceV1 from '../foo/v1';
This example demonstrates how you can organize runtime schemas to prepare for:
- Being versioned
- Have TypeScript references available to client and server code in your plugin
See strategies for versioning your schemas for more information on this organizational pattern.
// Somewhere in your plugin's server/routes folder
import { schema, TypeOf } from '@kbn/config-schema';
import type { FooResource } from '../../../common';
import { fooResource } from '../../schemas/v1';
// Note: this response schema is instantiated lazily to avoid creating schemas that are not needed in most cases!
const fooResourceResponse = () => {
return schema.object({
id: schema.string({
maxLength: 20,
meta: { description: 'Add a description.' }
}),
name: schema.string({ meta: { description: 'Add a description.' } }),
createdAt: schema.string({
meta: {
description: 'Add a description.',
deprecated: true,
},
}),
})
}
// Note: TypeOf can extract types for lazily instantiated schemas
type FooResourceResponse = TypeOf<typeof fooResourceResponse>
function registerFooRoute(router: IRouter, docLinks: DoclinksStart) {
router.versioned
.post({
path: '/api/foo',
access: 'public',
summary: 'Create a foo resource'
description: `A foo resource enables baz. See the following [documentation](${docLinks.links.fooResource}).`,
deprecated: true,
options: {
tags: ['oas-tag:my tag'],
availability: {
since: '1.0.0',
stability: 'experimental',
},
},
})
.addVersion({
version: '2023-10-31',
validate: {
request: {
body: fooResource,
},
response: {
200: {
description: 'Indicates a successful call.',
body: fooResourceResponse,
},
},
},
},
async (ctx, req, res) => {
const core = await ctx.core;
const savedObjectsClient = core.savedObjects.client;
const body = req.body;
const foo = await createFoo({ name: body.name });
// This is our HTTP translation layer to ensure only the necessary fields included
const responseBody: FooResourceResponse = {
id: foo.id,
name: foo.name,
createdAt: foo.createdAt,
};
return res.ok({ body: responseBody });
}
);
}
- An indicator that the property is deprecated
- An indicator that the operation is deprecated
- Each operation must have a tag that's used to group similar endpoints in the docs
- The version that the API was added.
- The current lifecycle: experimental, beta, or stable
Beyond the schema of requests and responses, it is very useful to provide concrete requests and responses as examples. Examples go beyond defaults and provide a more intuitive understanding for end users in learning the behaviour of your API. See the bump.sh documentation for more information on how examples will be shown to end users.
To add examples to the endpoint we created above you could do the following:
// ...
.addVersion({
version: '2023-10-31',
options: {
// Be sure and lazily instantiate this value. It's only used at dev time!
oasOperationObject: () => ({
requestBody: {
content: {
'application/json': {
examples: {
fooExample1: {
summary: 'An example foo request',
value: {
name: 'Cool foo!',
} as FooResource,
},
},
},
},
},
responses: {
200: {
content: {
'application/json': {
examples: {
/* Put your 200 response examples here */
},
},
},
},
},
}),
},
validate: {
request: {
body: fooResource,
},
response: {
200: {
body: fooResourceResponse,
},
},
},
},
// ...
The strength of this approach is your examples are captured in code and type checked at dev time. So any shape errors should be caught as you author.
<details>
<summary>I have prexisting YAML based examples I'd like to use!</summary>
If you pre-existing examples created in YAML that you would like to use the following approach:
import path from 'node:path';
const oasOperationObject: () => path.join(__dirname, 'foo.examples.yaml'),
// ...
.addVersion({
version: '2023-10-31',
options: {
oasOperationObject,
},
validate: {
request: {
body: fooResource,
},
response: {
200: {
body: fooResourceResponse,
},
},
},
},
// ...
Where the contents of foo.examples.yaml are:
requestBody:
content:
application/json:
examples:
fooExample:
summary: Foo example
description: >
An example request of creating foo.
value:
name: 'Cool foo!'
fooExampleRef:
# You can use JSONSchema $refs to organize this file further
$ref: "./examples/foo_example_i_factored_out_of_this_file.yaml"
responses:
200:
content:
application/json:
examples:
# Apply a similar pattern to writing examples here
x-codeSamples:
- lang: cURL
# label: A label which will be used as a title. Defaults to the lang value.
source: |
curl \
-X POST /api/foo
-H "kbn-xsrf: true"
-d '{...}'
- lang: Console
source: |
POST kbn:/api/agent_builder/tools
{...}
- Make sure to use the examples array, example (singular) has been deprecated
</details>
See <a href="#how-do-i-see-my-http-apis-oas">this section</a> about viewing your HTTP APIs OAS.
From here, you can develop your route and schema definitions iteratively. After each change the Kibana server will automatically reload and the latest OAS should reflect the current state of your code!
For example, let's add a few descriptions to our schema members:
const fooResourceResponse = () => {
return schema.object({
id: schema.string({ maxLength: 20, meta: { description: 'An unique ID for a foo resource.'} }),
name: schema.string({ meta: { description: 'A human friendly name for a foo resource.'} }),
createdAt: schema.string({ meta: { description: 'The ISO date a foo resource was created.'} }),
})
}
This descriptions should now be reflected in the OAS generated for your route.
You can also attach availability metadata on individual fields in code-first schemas. In @kbn/config-schema, use meta.availability; with Zod v4 (@kbn/zod/v4), use .meta({ openapi: { availability: ... } }). The OpenAPI generator maps this to an x-state extension on the corresponding schema property (or named component).
// @kbn/config-schema
schema.string({
meta: {
description: 'Add a description.',
availability: { stability: 'stable', since: '9.4.0' },
},
});
// @kbn/zod/v4
import { z } from '@kbn/zod/v4';
z.string().meta({
openapi: {
availability: { stability: 'stable', since: '9.4.0' },
},
});
For example, stability: 'stable' together with since: '9.4.0' becomes x-state: Generally available; added in 9.4.0 on that field in the generated document.
OAS for public routes are written to the Kibana repo as a snapshot that will ultimately be published.
At the time of writing we only capture OAS for a subset of Kibana's HTTP APIs to give teams time to check and improve the quality of generated OAS.
If you would like OAS for your endpoints to be included in the snapshot, please reach out to the Kibana Core team or follow the instructions below.
To publish OAS to our docs site create a pull request updating this command to include your HTTP API path.
The OAS will be pushed and published to our stateful and serverless docs hosted by bump.sh.
If you would like to preview your docs before merging, you can do the following:
- Install the bump cli: https://www.npmjs.com/package/bump-cli
- Save your docs to a local file
curl localhost:5601/api/oas\?access\=public\&version\=2023-10-31\&pathStartsWith\=/api/saved_objects/_export > temp.json npx bump preview temp.json- Once done, your docs should be hosted at a temporary location provided by bump.sh
Teams have adopted different runtime validation libraries for their HTTP APIs. Kibana core does not intend to support all runtime validation libraries.
Reach out to the Kibana Core Team with questions, concerns or issues you may be facing with @kbn/config-schema and we will help you find a solution.
It's possible to generate OpenAPI specification for access: 'internal' routes but it is not required. The benefit will largely be for your team's internal reference and for other teams to discover your APIs. If you follow the practices outlined in this tutorial it should be simple to generate OAS for internal routes as well.