JSON API Documentation: OpenAPI, JSON Schema Examples & Auto-Generation
Last updated:
Most JSON API documentation guides stop at "add Swagger UI to your Express app." This guide goes deeper: how OpenAPI 3.1 aligns with JSON Schema 2020-12 (and why the 3.0 differences matter), how to write JSON Schema examples that render correctly in Swagger UI and Redoc, how to auto-generate OpenAPI docs from Zod schemas and TypeScript types, how to document discriminated unions with oneOf, and how to keep documentation in sync with implementation using contract testing tools like Dredd and Prism. Every pattern comes with a code sample and a concrete reason why it matters for API consumers.
OpenAPI 3.1 and JSON Schema Alignment
OpenAPI 3.0.x extended JSON Schema draft-04 with incompatible keywords: nullable: true, discriminator, and example were OpenAPI inventions not present in any JSON Schema draft. This meant you could not pass an OpenAPI 3.0 schema object directly to a JSON Schema validator — tools had to maintain custom preprocessing logic. OpenAPI 3.1.0 eliminates this split by fully adopting JSON Schema draft 2020-12.
# OpenAPI 3.0.x — nullable used OpenAPI-specific keyword
components:
schemas:
User:
type: object
properties:
email:
type: string
nullable: true # OpenAPI-only, not valid JSON Schema
# OpenAPI 3.1.0 — uses JSON Schema 2020-12 type array
components:
schemas:
User:
type: object
properties:
email:
type: ["string", "null"] # valid JSON Schema 2020-12# openapi.yaml — minimal 3.1 document structure
openapi: "3.1.0"
info:
title: Orders API
version: "1.0.0"
paths:
/orders/{id}:
get:
operationId: getOrder
summary: Retrieve an order by ID
parameters:
- name: id
in: path
required: true
schema:
type: integer
responses:
"200":
description: Order found
content:
application/json:
schema:
$ref: "#/components/schemas/Order"
"404":
description: Order not found
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
/orders:
post:
operationId: createOrder
summary: Create a new order
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/CreateOrderInput"
responses:
"201":
description: Order created
content:
application/json:
schema:
$ref: "#/components/schemas/Order"
"400":
description: Validation error
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
components:
schemas:
Order:
type: object
required: [id, status, amount]
properties:
id:
type: integer
description: Unique order identifier
example: 1001
status:
type: string
enum: [pending, paid, shipped, cancelled]
description: Current order status
amount:
type: number
minimum: 0
description: Order total in USD
note:
type: ["string", "null"] # nullable in OpenAPI 3.1 — valid JSON Schema
description: Optional customer note
$comment: "Nullable field — null means no note provided, absent means not fetched"
CreateOrderInput:
type: object
required: [amount]
properties:
amount:
type: number
minimum: 0.01
note:
type: string
Error:
type: object
required: [code, message]
properties:
code:
type: string
message:
type: stringIn OpenAPI 3.1, the schema object inside requestBody and responses is a full JSON Schema 2020-12 document. This means you can use $ref alongside other keywords, $defs for local definitions, if/then/else, unevaluatedProperties, and the $comment field for internal annotations that do not appear in rendered documentation but persist in the spec file. For JSON Schema and OpenAPI interaction patterns, see the linked guide.
Writing JSON Schema Examples in OpenAPI
OpenAPI 3.x provides two distinct mechanisms for JSON examples. The singular example keyword (inline, on a schema or media type object) and the plural examples keyword (named map of example objects with summary, description, and value). Swagger UI and Redoc render these differently — understanding the distinction prevents documentation that looks correct in source but confuses consumers in the rendered UI.
# Schema-level example (single, inline — appears as default in Swagger UI)
components:
schemas:
Order:
type: object
required: [id, status, amount]
properties:
id:
type: integer
example: 1001 # field-level example
status:
type: string
enum: [pending, paid, shipped, cancelled]
example: paid # field-level example
amount:
type: number
example: 149.99
example: # object-level example (overrides field examples in UI)
id: 1001
status: paid
amount: 149.99
note: "Rush order"# Media type-level examples (plural — renders as dropdown in Swagger UI)
paths:
/orders/{id}:
get:
responses:
"200":
description: Order found
content:
application/json:
schema:
$ref: "#/components/schemas/Order"
examples:
paid_order:
summary: Paid order with note
value:
id: 1001
status: paid
amount: 149.99
note: "Rush delivery"
pending_order:
summary: Pending order without note
value:
id: 1002
status: pending
amount: 29.99
note: null
cancelled_order:
summary: Cancelled order
value:
id: 1003
status: cancelled
amount: 0
note: null
post:
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/CreateOrderInput"
examples:
minimal:
summary: Minimal order (amount only)
value:
amount: 29.99
with_note:
summary: Order with customer note
value:
amount: 149.99
note: "Leave at door"
# Reusable examples in components (reference with $ref)
components:
examples:
PaidOrder:
summary: Paid order — full object
value:
id: 1001
status: paid
amount: 149.99
note: "Rush delivery"
# Reference the reusable example:
# examples:
# paid_order:
# $ref: "#/components/examples/PaidOrder"
# x-example extension (unofficial, some tools support it)
# Used for request parameter examples not covered by the examples object:
parameters:
- name: status
in: query
schema:
type: string
enum: [pending, paid, shipped]
example: paid # singular example on parameterA critical gotcha: if you define both a schema-level example and a media-type-level examples map on the same object, Swagger UI uses the examples map and ignores the schema-level example. The examples object takes precedence. Always use examples (plural) at the media type level when you want named scenarios visible in Swagger UI's dropdown. For JSON Schema examples best practices including annotation keywords, see the linked guide.
Auto-Generating JSON API Docs from Code
Manually maintaining an OpenAPI spec alongside application code is the primary cause of documentation drift. The solution is code-first documentation: derive the OpenAPI spec from the same type definitions or validation schemas that the application already uses at runtime. Three practical approaches cover tRPC auto-docs, Zod to OpenAPI via zod-openapi, and TypeScript to JSON Schema via ts-json-schema-generator.
// ── Approach 1: Zod to OpenAPI with zod-openapi ──────────────────────
// npm install zod-openapi zod
import { z } from 'zod'
import { extendZodWithOpenApi, createDocument } from 'zod-openapi'
extendZodWithOpenApi(z)
// Define schemas with OpenAPI metadata — single source of truth
const OrderSchema = z.object({
id: z.number().int().openapi({ description: 'Order ID', example: 1001 }),
status: z.enum(['pending', 'paid', 'shipped', 'cancelled']).openapi({ example: 'paid' }),
amount: z.number().min(0).openapi({ description: 'Total in USD', example: 149.99 }),
note: z.string().nullable().optional().openapi({ example: 'Rush delivery' }),
}).openapi('Order') // registers as components/schemas/Order
const CreateOrderInput = z.object({
amount: z.number().min(0.01).openapi({ example: 29.99 }),
note: z.string().optional().openapi({ example: 'Leave at door' }),
}).openapi('CreateOrderInput')
// Generate the full OpenAPI document
const document = createDocument({
openapi: '3.1.0',
info: { title: 'Orders API', version: '1.0.0' },
paths: {
'/orders/{id}': {
get: {
operationId: 'getOrder',
requestParams: { path: z.object({ id: z.string() }) },
responses: {
'200': {
description: 'Order found',
content: { 'application/json': { schema: OrderSchema } },
},
'404': { description: 'Order not found' },
},
},
},
'/orders': {
post: {
operationId: 'createOrder',
requestBody: {
content: { 'application/json': { schema: CreateOrderInput } },
},
responses: {
'201': {
description: 'Order created',
content: { 'application/json': { schema: OrderSchema } },
},
},
},
},
},
})
// Write to openapi.json for Swagger UI / Redoc
import fs from 'fs'
fs.writeFileSync('openapi.json', JSON.stringify(document, null, 2))// ── Approach 2: TypeScript to JSON Schema (ts-json-schema-generator) ──
// npm install --save-dev ts-json-schema-generator
// types.ts — your TypeScript type definitions
export type OrderStatus = 'pending' | 'paid' | 'shipped' | 'cancelled'
/**
* An order placed by a customer.
*/
export interface Order {
/** Unique order identifier @example 1001 */
id: number
status: OrderStatus
/** Order total in USD @minimum 0 */
amount: number
/** Optional customer note */
note: string | null
}
// Generate JSON Schema from TypeScript types
// CLI: npx ts-json-schema-generator --path types.ts --type Order
// Output: order-schema.json
// Or programmatically:
import { createGenerator } from 'ts-json-schema-generator'
const generator = createGenerator({
path: 'src/types.ts',
tsconfig: 'tsconfig.json',
type: 'Order', // TypeScript type to generate schema for
expose: 'all',
jsDoc: 'extended', // use JSDoc comments for descriptions and examples
additionalProperties: false,
})
const schema = generator.createSchema('Order')
// schema is a valid JSON Schema — use in OpenAPI components/schemas// ── Approach 3: tRPC auto-docs with @trpc/openapi ─────────────────────
// npm install @trpc/openapi
import { initTRPC } from '@trpc/server'
import { OpenApiMeta } from '@trpc/openapi'
import { z } from 'zod'
const t = initTRPC.meta<OpenApiMeta>().create()
export const appRouter = t.router({
getOrder: t.procedure
.meta({
openapi: {
method: 'GET',
path: '/orders/{id}',
tags: ['orders'],
summary: 'Retrieve an order by ID',
},
})
.input(z.object({ id: z.string() }))
.output(OrderSchema) // your Zod schema — auto-generates response schema
.query(async ({ input }) => {
return getOrderById(input.id)
}),
createOrder: t.procedure
.meta({
openapi: { method: 'POST', path: '/orders', tags: ['orders'] },
})
.input(CreateOrderInput)
.output(OrderSchema)
.mutation(async ({ input }) => {
return createNewOrder(input)
}),
})
// Generate the OpenAPI document from the tRPC router
import { generateOpenApiDocument } from '@trpc/openapi'
const openApiDocument = generateOpenApiDocument(appRouter, {
title: 'Orders API',
version: '1.0.0',
baseUrl: 'https://api.example.com',
openApiVersion: '3.1.0',
})
// Serve via Express or Next.js API route
app.get('/openapi.json', (req, res) => res.json(openApiDocument))The key insight across all three approaches: the OpenAPI spec should be a build artifact derived from code, not a document maintained in parallel. When the Zod schema changes, the OpenAPI spec re-generates automatically. When the TypeScript type changes, the JSON Schema re-generates. This removes the human-error surface entirely. For tRPC JSON API patterns, see the linked guide.
JSON Request/Response Documentation Patterns
Documenting JSON API schemas accurately requires handling three patterns that trip up most OpenAPI authors: nullable fields (common in partial updates and optional relationships), discriminated unions (where the shape of the JSON varies based on a type field), and schema composition with oneOf/anyOf/allOf. Getting these right determines whether generated client SDKs and documentation are actually usable.
# ── Nullable fields (OpenAPI 3.1 / JSON Schema 2020-12) ───────────────
components:
schemas:
Order:
type: object
properties:
# Required, never null
id:
type: integer
# Optional, can be absent (not in response) or null (explicitly no value)
cancelledAt:
type: ["string", "null"]
format: date-time
description: |
Null if order is not cancelled.
Absent if cancellation data was not requested.
# Required in response, but value can be null
note:
type: ["string", "null"]
description: Customer note. Null means no note was provided.
# ── Discriminated union with oneOf + discriminator ─────────────────────
PaymentMethod:
oneOf:
- $ref: "#/components/schemas/CreditCardPayment"
- $ref: "#/components/schemas/BankTransferPayment"
- $ref: "#/components/schemas/WalletPayment"
discriminator:
propertyName: type
mapping:
credit_card: "#/components/schemas/CreditCardPayment"
bank_transfer: "#/components/schemas/BankTransferPayment"
wallet: "#/components/schemas/WalletPayment"
CreditCardPayment:
type: object
required: [type, cardNumber, expiryMonth, expiryYear]
properties:
type:
type: string
const: credit_card # const enforces the discriminator value
cardNumber:
type: string
pattern: "^\d{16}$"
expiryMonth:
type: integer
minimum: 1
maximum: 12
expiryYear:
type: integer
BankTransferPayment:
type: object
required: [type, routingNumber, accountNumber]
properties:
type:
type: string
const: bank_transfer
routingNumber:
type: string
pattern: "^\d{9}$"
accountNumber:
type: string
WalletPayment:
type: object
required: [type, walletId]
properties:
type:
type: string
const: wallet
walletId:
type: string
format: uuid
# ── allOf — composition / inheritance ──────────────────────────────────
# Base schema with shared fields
BaseItem:
type: object
required: [id, createdAt]
properties:
id:
type: integer
createdAt:
type: string
format: date-time
# Extended schema adds more fields (allOf = merge all schemas)
Product:
allOf:
- $ref: "#/components/schemas/BaseItem"
- type: object
required: [name, price]
properties:
name:
type: string
price:
type: number
minimum: 0
# ── anyOf — multiple valid shapes (not mutually exclusive) ─────────────
# Request that accepts either an ID or a slug
OrderLookup:
anyOf:
- type: object
required: [id]
properties:
id: { type: integer }
- type: object
required: [slug]
properties:
slug: { type: string }The discriminator object in OpenAPI is optional but strongly recommended: without it, validators and code generators must evaluate all oneOf branches for every document. With it, they can jump directly to the branch matching the propertyName field value. The mapping sub-object is also optional — if omitted, OpenAPI assumes the discriminator values match the schema component names exactly. Always include mapping explicitly for clarity and tool compatibility. For JSON Schema discriminator patterns, see the linked guide.
Swagger UI and Redoc for JSON APIs
Swagger UI and Redoc both render OpenAPI JSON/YAML specs as interactive documentation, but with different philosophies. Swagger UI prioritizes the "try it out" interactive testing experience. Redoc prioritizes a clean three-panel reading experience (navigation, main content, code samples) with no configuration required. Choosing between them — or serving both — depends on whether API consumers are primarily developers who need to test endpoints or readers who need to understand the API structure.
// ── Serving Swagger UI via Next.js API route ──────────────────────────
// app/api/docs/route.ts
import { NextResponse } from 'next/server'
// Serve the OpenAPI JSON spec
export async function GET() {
const spec = await import('@/openapi.json')
return NextResponse.json(spec)
}
// app/docs/page.tsx — render Swagger UI
// npm install swagger-ui-react
'use client'
import SwaggerUI from 'swagger-ui-react'
import 'swagger-ui-react/swagger-ui.css'
export default function DocsPage() {
return (
<SwaggerUI
url="/api/docs"
persistAuthorization={true} // keep auth token across page reloads
tryItOutEnabled={true} // auto-enable "Try it out" mode
filter={true} // show search/filter box
deepLinking={true} // URL hash links to specific operations
/>
)
}// ── Redoc — zero-config three-panel documentation ─────────────────────
// npm install redoc
// app/redoc/page.tsx
'use client'
import { RedocStandalone } from 'redoc'
export default function RedocPage() {
return (
<RedocStandalone
specUrl="/api/docs"
options={{
nativeScrollbars: true,
theme: { colors: { primary: { main: '#1d4ed8' } } },
hideDownloadButton: false,
expandResponses: '200,201', // auto-expand success responses
pathInMiddlePanel: true, // show path in center panel
}}
/>
)
}
// ── Standalone Redoc HTML (no framework required) ─────────────────────
// redoc-static.html
/*
<!DOCTYPE html>
<html>
<head>
<title>API Documentation</title>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700" rel="stylesheet">
<style>body { margin: 0; padding: 0; }</style>
</head>
<body>
<redoc spec-url="openapi.json"></redoc>
<script src="https://cdn.redoc.ly/redoc/latest/bundles/redoc.standalone.js"></script>
</body>
</html>
*/# ── Authentication in Swagger UI — securitySchemes ────────────────────
# openapi.yaml
components:
securitySchemes:
BearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
description: "Obtain a JWT from POST /auth/token. Enter as: Bearer <token>"
ApiKeyAuth:
type: apiKey
in: header
name: X-API-Key
OAuth2:
type: oauth2
flows:
authorizationCode:
authorizationUrl: https://auth.example.com/oauth/authorize
tokenUrl: https://auth.example.com/oauth/token
scopes:
read:orders: Read orders
write:orders: Create and update orders
# Apply globally to all operations:
security:
- BearerAuth: []
# Or per-operation (overrides global):
paths:
/orders:
post:
security:
- BearerAuth: []
- ApiKeyAuth: [] # either auth method accepted
# ...In Swagger UI, clicking "Authorize" opens a dialog populated from your securitySchemes. Users enter their token once and it is attached to all "Try it out" requests. With persistAuthorization: true, the token is stored in localStorage and survives page reloads — useful for developer portals where engineers test repeatedly. Redoc renders security schemes in the right panel alongside each operation but does not have an interactive try-it-out feature — pair it with a separate API client like the Redocly VS Code extension for interactive testing. For JSON OAuth and token handling patterns, see the linked guide.
JSON API Changelog and Deprecation Documentation
API deprecation without a clear timeline and migration path causes the worst kind of consumer churn: developers ignore the warnings, then face emergency changes when the endpoint actually goes offline. OpenAPI provides the deprecated: true flag for both operations and individual schema properties. The Sunset HTTP header (RFC 8594) communicates the removal date programmatically to clients. Custom extensions like x-deprecation-date document the timeline in the spec itself.
# ── Deprecated endpoint in OpenAPI spec ──────────────────────────────
paths:
/v1/orders/{id}:
get:
operationId: getOrderV1
deprecated: true
summary: "[DEPRECATED] Get order by ID"
description: |
**Deprecated as of 2026-02-10. Sunset date: 2026-12-31.**
Migrate to [GET /v2/orders/{id}](/v2/orders/{id}) which returns
structured error objects and supports field selection via ?fields=.
See the [V1 to V2 migration guide](https://docs.example.com/v2-migration).
x-deprecation-date: "2026-12-31"
x-migration-guide: "https://docs.example.com/v2-migration"
responses:
"200":
description: Order (legacy format)
content:
application/json:
schema:
$ref: "#/components/schemas/OrderV1"
/v2/orders/{id}:
get:
operationId: getOrderV2
summary: Get order by ID
responses:
"200":
description: Order (current format)
content:
application/json:
schema:
$ref: "#/components/schemas/OrderV2"
# ── Deprecated field within a schema ─────────────────────────────────
components:
schemas:
OrderV2:
type: object
properties:
id:
type: integer
# Deprecated field — use phoneNumbers array instead
phone:
type: string
deprecated: true
description: |
**Deprecated.** Use the phoneNumbers array instead.
Will be removed in API version 3.0.
# Replacement field
phoneNumbers:
type: array
items:
type: object
properties:
type: { type: string, enum: [mobile, home, work] }
number: { type: string }// ── Sunset header in Express/Next.js middleware ───────────────────────
// middleware that adds Sunset header to all deprecated v1 routes
// Express middleware
app.use('/v1', (req, res, next) => {
// RFC 8594 Sunset header — ISO 8601 date in HTTP-date format
res.set('Sunset', 'Wed, 31 Dec 2026 23:59:59 GMT')
// Link header pointing to sunset policy document (RFC 8288)
res.set('Link', '<https://docs.example.com/v2-migration>; rel="sunset", <https://docs.example.com/v2-migration>; rel="successor-version"')
// Deprecation header (draft RFC)
res.set('Deprecation', 'Thu, 10 Feb 2026 00:00:00 GMT')
next()
})
// Next.js middleware (middleware.ts)
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
if (request.nextUrl.pathname.startsWith('/api/v1/')) {
const response = NextResponse.next()
response.headers.set('Sunset', 'Wed, 31 Dec 2026 23:59:59 GMT')
response.headers.set(
'Link',
'<https://docs.example.com/v2-migration>; rel="sunset"'
)
return response
}
}
export const config = {
matcher: '/api/v1/:path*',
}// ── Changelog in OpenAPI with x-changelog extension ──────────────────
// openapi.yaml info section
info:
title: Orders API
version: "2.1.0"
x-changelog:
- version: "2.1.0"
date: "2026-05-20"
changes:
- type: added
description: "Added phoneNumbers array to Order schema"
- type: deprecated
description: "Deprecated phone field on Order — use phoneNumbers"
- version: "2.0.0"
date: "2026-02-10"
changes:
- type: breaking
description: "Replaced V1 order format with structured V2 response"
- type: added
description: "Added field selection via ?fields= query parameter"
- version: "1.0.0"
date: "2025-06-01"
changes:
- type: added
description: "Initial release"The deprecated: true flag in Swagger UI renders a strikethrough on the operation and a "DEPRECATED" badge. Redoc grays out deprecated operations. Neither tool shows deprecation warnings inline in the response body — that is the role of the Deprecation and Sunset HTTP headers, which well-behaved API client libraries can detect and log as warnings to developer consoles. For JSON API versioning strategies including URL vs header versioning, see the linked guide.
Testing Documentation Accuracy with Contract Testing
An OpenAPI spec that does not match the actual API implementation is worse than no documentation — it sends developers down dead ends. Contract testing uses the spec as the source of truth and verifies the implementation against it automatically, catching drift before it reaches consumers. Three tools cover the testing workflow: Dredd for spec-to-server validation, Prism for mock-based frontend development, and Spectral for spec quality linting.
# ── Dredd: test your server against the OpenAPI spec ─────────────────
# npm install --save-dev dredd
# dredd reads the spec, sends real requests, validates responses
# dredd.yml — configuration file
dry-run: false
hookfiles: ./dredd-hooks.js
language: nodejs
sandbox: false
server: npm start # how to start your server
server-wait: 3 # seconds to wait for server startup
endpoint: http://localhost:3000 # server URL
path:
- ./openapi.yaml # spec file to test
# Run Dredd:
# npx dredd
# Example Dredd output when server response matches spec:
# pass: GET /orders/{id} > 200
# pass: POST /orders > 201
# pass: POST /orders > 400
# Example Dredd failure (response body does not match schema):
# fail: GET /orders/{id} > 200
# - body: At '/note' expected type string but got null
# - (response body does not match schema)
# ── dredd-hooks.js: set up test data before requests ─────────────────
const hooks = require('hooks')
hooks.before('Orders > GET /orders/{id} > 200', (transaction, done) => {
// Insert test order and set the ID in the request path
transaction.fullPath = transaction.fullPath.replace('{id}', '1001')
done()
})
hooks.after('Orders > POST /orders > 201', (transaction, done) => {
// Clean up created test data
const body = JSON.parse(transaction.real.body)
// deleteOrder(body.id)
done()
})# ── Prism: mock server + contract validation ──────────────────────────
# npm install --save-dev @stoplight/prism-cli
# Start mock server from OpenAPI spec (responds with example values):
npx @stoplight/prism-cli mock openapi.yaml
# Mock server starts at http://127.0.0.1:4010
# Test with curl:
# curl http://localhost:4010/orders/1001
# Prism returns the first defined example or generates from schema
# Dynamic mode — generate random valid data every request:
npx @stoplight/prism-cli mock --dynamic openapi.yaml
# Specific port:
npx @stoplight/prism-cli mock -p 8080 openapi.yaml
# Proxy mode — validate real API responses against spec:
npx @stoplight/prism-cli proxy openapi.yaml http://localhost:3000
# Now send requests to http://localhost:4010 (Prism proxy)
# Prism forwards to :3000 and validates:
# - Request body against requestBody schema
# - Response body against response schema
# - Reports mismatches as errors with JSON detail
# package.json scripts for CI
{
"scripts": {
"mock": "prism mock openapi.yaml",
"mock:dynamic": "prism mock --dynamic openapi.yaml",
"validate-api": "prism proxy openapi.yaml http://localhost:3000"
}
}# ── Spectral: OpenAPI spec linting ────────────────────────────────────
# npm install --save-dev @stoplight/spectral-cli
# .spectral.yaml — ruleset configuration
extends:
- spectral:oas # built-in OpenAPI ruleset
rules:
# Require all operations to have an operationId
operation-operationId: error
# Require all operations to have at least one 4xx error response
operation-4xx-response:
description: Operations must document at least one 4xx error response.
given: "$.paths[*][get,post,put,patch,delete]"
severity: warn
then:
function: schema
functionOptions:
schema:
properties:
responses:
type: object
patternProperties:
"^4": {}
required: [responses]
# Require examples on all response schemas
response-examples-required:
description: All response schemas must have examples defined.
given: "$.paths[*][*].responses[*].content[*]"
severity: warn
then:
field: examples
function: truthy
# Run linting:
npx @stoplight/spectral-cli lint openapi.yaml
# CI integration (GitHub Actions)
# .github/workflows/api-docs.yml
# - name: Lint OpenAPI spec
# run: npx @stoplight/spectral-cli lint openapi.yaml --fail-on-errors
#
# - name: Run Dredd contract tests
# run: |
# npm start &
# sleep 3
# npx dreddThe recommended CI pipeline: (1) Spectral linting on every pull request — catches missing examples, undocumented error responses, and style violations before review. (2) Dredd contract tests on every merge to main — validates that the running server matches the spec. (3) Prism proxy tests as part of your integration test suite — validates every test request/response pair against the spec. This three-layer approach makes documentation drift operationally impossible rather than just discouraged. For JSON API testing patterns including integration and load testing, see the linked guide.
Key Terms
- OpenAPI Specification
- A language-agnostic standard for describing HTTP APIs as machine-readable JSON or YAML documents. An OpenAPI document describes endpoints (paths), HTTP methods, request parameters, request body schemas, response schemas, authentication schemes, and example values. Version 3.1.0 (released 2021) is the current major version and fully adopts JSON Schema draft 2020-12 for schema objects. OpenAPI documents can be used to generate documentation (Swagger UI, Redoc), server stubs, client SDKs (in 40+ languages via OpenAPI Generator), mock servers (Prism), and to run contract tests (Dredd). The specification is maintained by the OpenAPI Initiative, a Linux Foundation project. Files are conventionally named
openapi.yamloropenapi.json. - JSON Schema
- A vocabulary for annotating and validating JSON documents. A JSON Schema document is itself JSON, describing the expected type (
type), allowed values (enum,const), structure (properties,required,additionalProperties), numeric constraints (minimum,maximum), string constraints (minLength,pattern,format), and array constraints (items,minItems,maxItems) of a JSON value. The current draft is 2020-12. JSON Schema is used in OpenAPI 3.1 schema objects, in IDE validation for JSON config files (VS Code uses JSON Schema forpackage.json,tsconfig.json, etc.), and as the basis for runtime validation libraries like Ajv. The$commentkeyword allows internal annotations that do not affect validation. - requestBody
- An OpenAPI 3.x object that describes the HTTP request body for operations that accept one (typically POST, PUT, PATCH). The
requestBodyobject contains acontentmap keyed by media type (e.g.,application/json), each containing aschemaobject describing the expected JSON structure and optionally anexamplesmap with named example payloads. Therequired: trueflag indicates the body is mandatory. In contrast, OpenAPI 2.0 (Swagger) used abodyparameter — this changed torequestBodyin 3.0 to cleanly separate body from query/path/header parameters. Swagger UI renders the requestBody schema as a model and populates the "Try it out" body editor with the first defined example. - Discriminated Union (oneOf)
- A JSON Schema and OpenAPI pattern where a value can match exactly one of several schemas, identified by a distinguishing field. In OpenAPI, this is expressed with
oneOf(exactly one schema must match) combined with adiscriminatorobject specifying thepropertyNamethat identifies which variant applies. For example, aPaymentMethodschema usestypeas the discriminator: iftype === "credit_card", theCreditCardPaymentschema applies; iftype === "bank_transfer", theBankTransferPaymentschema applies. The discriminator enables validators and code generators to jump to the correct schema branch without testing all variants, and enables Swagger UI to render a dropdown for selecting the variant to document or test.anyOfmeans zero or more schemas match;allOfmeans all listed schemas must match (used for composition). - Swagger UI
- An open-source JavaScript library and web application that renders OpenAPI JSON/YAML specs as interactive API documentation in a browser. Key features: operation listing with collapsible panels, schema rendering with type and constraint details, "Try it out" mode for sending real HTTP requests from the browser, authorization dialogs populated from
securitySchemes, and an examples dropdown for operations with multiple named examples. Available as a standalone Docker image, an npm package (swagger-ui-reactfor React,swagger-ui-expressfor Express), and embedded in API gateway products. Swagger UI prioritizes interactive testing; Redoc prioritizes reading comprehension. Both can be served from the same OpenAPI spec simultaneously. - Redoc
- An open-source documentation renderer that generates a three-panel documentation site from any OpenAPI 2.x or 3.x JSON/YAML spec with zero configuration. The left panel shows navigation (paths grouped by tag), the center panel shows operation details (description, parameters, request/response schemas), and the right panel shows code samples and example payloads. Redoc is available as a standalone JavaScript file (CDN, no framework needed), an npm package (
redoc,redoc-cli), and a React component (RedocStandalone). Theredoc-cli bundlecommand generates a static single-file HTML documentation site for hosting without a server. Redoc does not include a "Try it out" testing interface — it is a reading tool. Thex-codeSamplesOpenAPI extension adds language-specific code samples to the right panel. - Contract Testing
- A testing methodology where the API contract (the OpenAPI spec or similar agreement) is used as the specification that both the server implementation and client consumers must satisfy, verified by automated tests. Unlike integration tests that test specific scenarios, contract tests verify that the API surface — all documented endpoints, schemas, status codes, and headers — matches what the spec says. Tools include Dredd (sends requests to a real server and validates responses against the OpenAPI spec), Prism in proxy mode (intercepts requests to a real server and validates both request and response against the spec), and Pact (consumer-driven contract testing where consumers define their expectations). Contract testing is most valuable in CI pipelines where it catches spec drift before it reaches API consumers.
- Prism Mock Server
- A command-line tool by Stoplight that reads an OpenAPI spec and starts an HTTP mock server that responds to documented endpoints with JSON payloads derived from the spec's examples or auto-generated from the schema. Run with
npx @stoplight/prism-cli mock openapi.yaml— the server starts in under one second athttp://127.0.0.1:4010. Response priority: (1) the firstexamplesvalue matching the requested media type, (2) schema-generated data. With--dynamic, Prism generates randomized but schema-valid data on every request. In proxy mode (prism proxy), it forwards requests to a real server and validates both request and response against the spec, reporting mismatches. Prism is used for frontend development before the backend is ready and for contract validation in CI pipelines. - Deprecation (API)
- The formal process of marking an API endpoint, schema property, or parameter as scheduled for removal in a future version, with sufficient advance notice for consumers to migrate. In OpenAPI, the
deprecated: trueflag marks operations and properties. TheSunsetHTTP response header (RFC 8594) communicates the removal date programmatically:Sunset: Wed, 31 Dec 2026 23:59:59 GMT. TheDeprecationheader (draft RFC) indicates when the deprecation was announced. TheLinkheader withrel="sunset"points to a policy document or migration guide. Industry practice is a minimum 6-month deprecation window for public APIs, 12 months for widely-used endpoints. Custom OpenAPI extensions likex-deprecation-datedocument the timeline in the spec file for documentation tools to render.
FAQ
What is the difference between OpenAPI 3.0 and 3.1 for JSON schema documentation?
OpenAPI 3.0.x used a modified subset of JSON Schema draft-04 with proprietary extensions — the most painful being nullable: true, which has no equivalent in any JSON Schema draft. This meant you could not use standard JSON Schema validators directly on OpenAPI 3.0 schema objects. OpenAPI 3.1.0 fully adopts JSON Schema draft 2020-12, eliminating this split. Nullable fields now use the standard type array: type: ["string", "null"]. The $ref keyword can now appear alongside other keywords (previously forbidden in 3.0). Schema objects are valid JSON Schema 2020-12 documents, so validators like Ajv with the draft2020 option work directly. The practical benefit: maintain one JSON Schema file per model, reference it from both your OpenAPI spec (via $ref) and your application's Zod or Ajv validation — a true single source of truth. The tradeoff: tool support for OpenAPI 3.1 is still catching up to 3.0, though Swagger UI, Redoc, and most major tools now support it fully.
How do I auto-generate OpenAPI documentation from Zod schemas?
Use the zod-openapi package. Install: npm install zod-openapi. Call extendZodWithOpenApi(z) once at startup, then attach metadata to schemas with .openapi({ description, example }). Register a schema as a named component with .openapi('SchemaName') at the schema level. Generate the full document with createDocument({ openapi, info, paths }) — passing your Zod schemas as schema values in request body and response content objects. The library converts Zod types to JSON Schema: z.string().email() becomes { "type": "string", "format": "email" }, z.enum(['a','b']) becomes { "type": "string", "enum": ["a","b"] }, z.object({...}) becomes a full object schema with required and properties. The result is a valid OpenAPI 3.1 document where components/schemas are derived entirely from your Zod definitions — the same schemas that validate runtime requests and responses. Any Zod change propagates to the OpenAPI doc automatically.
How do I document discriminated union types (oneOf) in an OpenAPI JSON schema?
Use oneOf with a discriminator object. Define a parent schema with oneOf listing $ref references to each variant. Add a discriminator object with propertyName (the JSON field that identifies the variant) and an optional mapping (a map from discriminator values to schema references). Each variant schema must include the discriminator property as a const string. Example: a PaymentMethod with type as discriminator; CreditCardPayment has type: { const: "credit_card" }, BankTransferPayment has type: { const: "bank_transfer" }. Swagger UI renders a dropdown to switch between variants. Validators can skip non-matching branches immediately using the discriminator value, improving performance. Without the discriminator, validators must test all oneOf branches for every document — functional but slower and produces less precise error messages. Always include the mapping object explicitly rather than relying on schema name matching, which is fragile when schema names change.
How do I write JSON examples in OpenAPI that appear in Swagger UI?
Define examples at the media type level using the examples keyword (plural), not the schema-level example keyword (singular). The examples object is a named map where each key is an example name and each value has summary (display label in Swagger UI dropdown), description (optional longer explanation), and value (the actual JSON payload). Place this object inside paths['/endpoint'][method].responses['200'].content['application/json'] or inside requestBody.content['application/json']. In Swagger UI, the dropdown appears automatically when two or more named examples exist. Reuse examples across operations by defining them in components/examples and referencing with $ref: '#/components/examples/MyExample'. Important: if both example (singular, on the schema) and examples (plural, on the media type) are present, Swagger UI uses examples and ignores the schema-level example. Validate that your example values actually conform to the schema using Spectral: npx @stoplight/spectral-cli lint openapi.yaml.
How do I deprecate a JSON API field or endpoint in OpenAPI documentation?
For an endpoint: add deprecated: true to the operation object alongside your existing summary and description. Update the summary to include "[DEPRECATED]" as a visual cue, and add a description block explaining the replacement and the sunset date. Swagger UI renders a strikethrough style and "DEPRECATED" badge. For a schema field: add deprecated: true to the specific property within components/schemas/YourSchema/properties. At the HTTP level, add the Sunset header (RFC 8594) to all responses from deprecated endpoints: Sunset: Wed, 31 Dec 2026 23:59:59 GMT. Add a Link header pointing to the migration guide: Link: <https://docs.example.com/v2>; rel="sunset". Add the custom extension x-deprecation-date: "2026-12-31" to the operation for documentation tools. Keep deprecated operations in the spec throughout the deprecation window — removing them from the spec before the sunset date leaves developers without documentation for their existing integrations.
How do I use Prism to generate a mock server from my OpenAPI JSON spec?
Run without installation: npx @stoplight/prism-cli mock openapi.yaml — or for a JSON spec: npx @stoplight/prism-cli mock openapi.json. The mock server starts at http://127.0.0.1:4010 in under one second. Test with curl: curl http://localhost:4010/orders/1001. Prism responds with: (1) the first examples value defined in your response schema for that status code, or (2) auto-generated JSON matching the schema type constraints if no example is defined. Use --dynamic for randomized valid data on every request: npx @stoplight/prism-cli mock --dynamic openapi.yaml. Change the port: npx @stoplight/prism-cli mock -p 8080 openapi.yaml. Prism also validates incoming request bodies against your requestBody schema and returns a 400 error for invalid JSON — useful for testing frontend error handling before the backend exists. Install as a dev dependency for consistent CI runs: npm install --save-dev @stoplight/prism-cli, then add "mock": "prism mock openapi.yaml" to your package.json scripts.
How do I keep my JSON API documentation in sync with the actual implementation?
Three complementary strategies work together. First, code-first generation: derive your OpenAPI spec from Zod schemas, TypeScript types, or tRPC routers that the application uses at runtime. If the spec is a build artifact from code, they cannot diverge — changing the Zod schema regenerates the spec automatically. Second, Dredd contract testing in CI: npm install --save-dev dredd, configure dredd.yml pointing at your spec and server URL, then run npx dredd in your CI pipeline. Dredd sends real HTTP requests to your running server and validates responses against the documented schemas — any mismatch is a test failure. Third, Prism proxy validation during integration testing: run npx @stoplight/prism-cli proxy openapi.yaml http://localhost:3000, then direct your test suite at the Prism proxy instead of the server directly. Every request/response pair is validated against the spec. Add Spectral linting to catch spec quality issues before they reach consumers: npx @stoplight/spectral-cli lint openapi.yaml in CI. The combination of all three — code-first generation, Dredd/Prism contract validation, and Spectral linting — makes documentation drift operationally impossible.
Further reading and primary sources
- OpenAPI Specification 3.1.0 — The official OpenAPI 3.1.0 specification — full reference for all keywords, schema objects, requestBody, responses, and discriminator
- zod-openapi GitHub Repository — Convert Zod schemas to OpenAPI 3.x components/schemas — single source of truth for validation and documentation
- Stoplight Prism Documentation — Prism mock server and proxy — generate mock responses and validate real API responses against an OpenAPI spec
- Dredd API Testing Framework — HTTP API testing tool that validates server behavior against OpenAPI and API Blueprint specifications
- Spectral OpenAPI Linter — Lint and validate OpenAPI specs for missing examples, undocumented errors, and style rule violations