GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • oRPC
  • Documentation
  • Blog
  • Comparison
  • From V1
  • Sponsors
  • Experts
  • Releases
  • Discussions
  • LLM Context
  • LLM Context (Full)
  • V1 Documentation
  • Contract-First
  • Requirements

This view

  • Original page
  • New address

GeezerVison is showing orpc.dev — OpenAPI Specification - oRPC

On this page
  • Metadata
  • Customizing the Operation Object
  • Metadata Merging
  • OpenAPI Generator
  • OpenAPI Version
  • Json Schema Converters
  • Custom Serializer
  • Filtering Procedures
  • Hoisting $defs

OpenAPI Specification

Learn how to configure openapi metadata and generate OpenAPI 3.2, 3.1, or 3.0 documents from your oRPC contracts and routers with OpenAPIGenerator.

Sponsors

ScreenshotOne.comThe screenshot API for developersYuzuWe're hiring NYC based engineersMisskeyHQDecentralized microblogging SNS born on Earth

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(),
    }),
  )

For routing metadata, you can learn more in OpenAPI Routing. For input and output mapping metadata, see OpenAPI Input and Output Mapping.

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. The operation object always follows OpenAPI 3.2, whatever version you generate.

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:

  • tags and prefix values are concatenated in definition order.
  • paramsStyles and queryStyles are merged per parameter. The most recent style defined for a parameter wins.
  • spec values 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.

Router-level metadata applies again, even if a procedure already has it:

const base = os.meta(openapi({ prefix: '/api' }))

const router = base.router({
  list: base.handler(() => []), // prefix: '/api/api'
})
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 () => []),
}

Metadata resets to its default behavior when set to undefined in subsequent calls:

const example = os.meta(openapi({ tags: ['planets'] })).meta(openapi({ tags: undefined }))

In this example, the final tags is undefined, so no tags are applied to example.

Sponsors

ScreenshotOne.comThe screenshot API for developersYuzuWe're hiring NYC based engineers

OpenAPI Generator

OpenAPIGenerator turns a contract or a router into an OpenAPI document.

import { OpenAPIGenerator } from '@orpc/openapi'

const generator = new OpenAPIGenerator({
  converters: [new ZodToJsonSchemaConverter()],
})

const spec = await generator.generate(router, {
  version: '3.2.0',
  base: {
    info: {
      title: 'Planet API',
      version: '1.0.0',
    },
    servers: [{ url: 'https://example.com/api' }],
  },
})

base provides the OpenAPI 3.2 document fields to start from, such as info, servers, or components. The openapi field comes from version.

OpenAPI Version

version selects the OpenAPI version, 3.2.0 by default. Any 3.0.x, 3.1.x, or 3.2.x value works.

const spec = await generator.generate(router, {
  version: '3.0.4',
})

The document is always built as OpenAPI 3.2, so base, openapi({ spec }), and every JSON schema follow 3.2. Older versions come from downgrading the whole document with @openapi-spec/downgrader, which converts what the older version can still express and removes the rest.

QUERY operations require OpenAPI 3.2. Generating an older version from a router with a QUERY procedure throws.

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(),
  ],
})

When no matching converter is configured, OpenAPIGenerator falls back to Standard Json Schema conversion. See Standard Schema Integration for details, including how to build your own converter.

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 componentName to rename them:

const spec = await generator.generate(router, {
  componentName: (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,
  },
  errorResponseBodySchema: (definedErrors, status, defaultSchema) => {
    if (status === 410) {
      return {
        type: 'object',
        properties: {
          code: { type: 'string' },
          message: { type: 'string' },
        },
        required: ['code', 'message'],
      }
    }

    return defaultSchema()
  },
})

If the handler sets errorResponseContentType, pass the same value to the generator so error responses are documented under that media type:

const spec = await generator.generate(router, {
  errorResponseContentType: 'application/problem+json',
})

Sponsors

ScreenshotOne.comThe screenshot API for developersYuzuWe're hiring NYC based engineers

Last updated on November 9, 2026

Was this page helpful?

Thanks for your feedback!

You are visitor 01402503 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.