OpenAPI Specification
Learn how to configure openapi metadata and generate OpenAPI documents from your oRPC contracts and routers with OpenAPIGenerator.
Metadata
Use openapi metadata to control how a procedure appears in the generated OpenAPI document:
import { oc } from '@orpc/contract'
import { openapi } from '@orpc/openapi'
import { z } from 'zod'
const getPlanet = oc
.meta(openapi({
method: 'GET',
path: '/planets/{id}',
operationId: 'getPlanet',
summary: 'Get a planet',
description: 'Returns a single planet.',
tags: ['planets'],
successStatus: 200,
successDescription: 'Planet payload',
}))
.input(z.object({
id: z.string(),
}))
.output(z.object({
id: z.string(),
name: z.string(),
}))
Customizing the Operation Object
Use spec to customize the generated operation object. If spec is an object, it replaces the generated operation object entirely. If spec is a callback, it receives the final operation object and returns an extended version.
const getPlanet = oc
.meta(openapi({
method: 'GET',
path: '/planets/{id}',
spec: current => ({
...current,
security: [{ bearerAuth: [] }],
}),
}))
.input(z.object({ id: z.string() }))
Metadata Merging
When openapi is applied multiple times, most fields, such as method, path, operationId, summary, and description, are overridden by the most recent call. Only the following fields are merged:
tagsandprefixvalues are concatenated in definition order.paramsStylesandqueryStylesare merged per parameter. The most recent style defined for a parameter wins.specvalues are combined: two functions are chained so the most recent one receives the result of the previous one, a function combined with an object is applied to that object, and between two objects the most recent one wins.
For implementation details, see the source code.
const router = os
.meta(openapi({
tags: ['planets'],
spec: current => ({
...current,
security: [{ bearerAuth: [] }],
}),
}))
.router({
list: os
.meta(openapi({ method: 'GET', summary: 'List planets', tags: ['list'] }))
.meta(openapi({
spec: {
operationId: 'getPlanet',
summary: 'List planets',
responses: {
200: {
description: 'List of planets',
},
}
}
}))
.input(z.object({ q: z.string().optional() }))
.handler(async () => ([])),
})
These are equivalent to:
const router = {
list: os
.meta(openapi({
method: 'GET',
tags: ['planets', 'list'],
summary: 'List planets',
spec: {
operationId: 'getPlanet',
summary: 'List planets',
responses: {
200: {
description: 'List of planets',
},
},
security: [{ bearerAuth: [] }],
},
}))
.input(z.object({ q: z.string().optional() }))
.handler(async () => ([])),
}
OpenAPI Generator
OpenAPIGenerator accepts either a contract or a router and generates an OpenAPI document. It uses OpenAPI 3.1.2 by default.
import { OpenAPIGenerator } from '@orpc/openapi'
const generator = new OpenAPIGenerator({
converters: [new ZodToJsonSchemaConverter()],
})
const spec = await generator.generate(router, {
base: {
info: {
title: 'Planet API',
version: '1.0.0',
},
servers: [
{ url: 'https://example.com/api' },
],
},
})
QUERY Operations
OpenAPI defines the query Path Item field starting in OpenAPI 3.2. When a router contains a QUERY operation, explicitly select OpenAPI 3.2 in the base document:
const spec = await generator.generate(router, {
base: {
openapi: '3.2.0',
info: {
title: 'Planet API',
version: '1.0.0',
},
},
})
Without base.openapi: '3.2.0', generation fails rather than emitting the 3.2-only query field in the default OpenAPI 3.1.2 document. Routers without QUERY operations continue to generate OpenAPI 3.1.2 documents by default.
oRPC’s public document type includes only the OpenAPI 3.2 compatibility needed for QUERY Path Items; it does not claim complete OpenAPI 3.2 coverage. OpenAPI viewers, client generators, validators, HTTP runtimes, proxies, and gateways may not support OpenAPI 3.2 or the QUERY method yet. Verify every tool and network hop used by your API before adopting it.
Json Schema Converters
OpenAPIGenerator relies on JSON Schema converters to translate your input, output, and error schemas into JSON Schemas. oRPC provides dedicated converters through the Zod, Valibot, and ArkType integrations:
import { ZodToJsonSchemaConverter } from '@orpc/zod'
import { ValibotToJsonSchemaConverter } from '@orpc/valibot'
import { ArkTypeToJsonSchemaConverter } from '@orpc/arktype'
const generator = new OpenAPIGenerator({
converters: [
new ZodToJsonSchemaConverter(),
new ValibotToJsonSchemaConverter(),
new ArkTypeToJsonSchemaConverter(),
],
})
Custom Serializer
If your OpenAPI Handler uses a custom serializer, configure OpenAPIGenerator with the same serializer so the generated document matches the actual formats. For details, see OpenAPI Serializer.
const handler = new OpenAPIGenerator({
serializer: new OpenAPISerializer({
handlers: {
// ...custom handlers
},
}),
})
Filtering Procedures
Use filter to exclude procedures from the generated document:
const spec = await generator.generate(router, {
filter: (_procedure, path) => !path.includes('internal'),
})
Hoisting $defs
Root $defs generated by your converters are moved into components.schemas. Use customComponentName to rename them:
const spec = await generator.generate(router, {
customComponentName: (defName, defSchema) => `Api${defName}`,
})
Custom Error Response Schemas
If your OpenAPI Handler uses custom error response formats, configure OpenAPIGenerator with the same logic so the generated document matches the actual error response formats.
import { COMMON_ERROR_STATUS_MAP } from '@orpc/openapi'
const spec = await generator.generate(router, {
errorStatusMap: {
...COMMON_ERROR_STATUS_MAP,
PLANET_GONE: 410,
},
customErrorResponseBodySchema: (definedErrors, status) => {
if (status === 410) {
return {
type: 'object',
properties: {
code: { type: 'string' },
message: { type: 'string' },
},
required: ['code', 'message'],
}
}
// fallback to default by returning null or undefined
return null
},
})