Managing Example Payloads in API Specs
This guide is part of OpenAPI & AsyncAPI Schema Authoring, and it tackles a problem that grows quietly until it hurts: example payloads. Good examples let portal users grasp a request or response shape at a glance, without decoding the schema. But inline examples bloat the spec, trigger merge conflicts, and silently drift out of sync with the schemas they illustrate. This page shows how to externalize examples with $ref and externalValue, enforce their presence in CI, and detect drift before it reaches your portal’s try-it-out console.
The same techniques apply to OpenAPI request/response bodies and AsyncAPI message payloads, because both use the JSON Schema Example Object model. Treat examples as part of the contract — versioned, validated, and reviewed — rather than as throwaway documentation. We will set up the directory layout, externalize references, wire a validation workflow, cover advanced version-aware rendering, and finish with the resolution and drift failures you will actually hit.
Prerequisites & Environment Setup
Pin the bundler and linter so example resolution behaves the same locally and in CI. Add ajv-cli for validating raw example files against compiled schemas.
node --version # v20.x LTS
npm install --save-dev \
@redocly/cli@^2 \
@stoplight/[email protected] \
ajv-cli ajv-formats
Keep examples out of the main spec and mirror the API’s versioning in the directory layout. Strict parity between endpoints and example files makes reviews obvious and lets you swap whole example sets per API version:
openapi.yaml
examples/
v1/
bulk_order.json
bulk_order_response.json
user_create.json
v2/
user_create.json # adds optional fields introduced in v2
.spectral.yaml
The schemas these examples illustrate should follow defining JSON Schema components — define the shape once under components/schemas, then attach examples by reference. Keeping schema and example concerns in separate files is what makes both independently reviewable.
Core Configuration
Inline examples blocks of any real size bloat the spec and cause merge conflicts on every edit. Externalize them. OpenAPI gives you two mechanisms, and they are not interchangeable: $ref pulls in an Example Object (with summary/description), while externalValue points at a raw payload file by URI.
# openapi.yaml (OpenAPI 3.1.0)
paths:
/orders/bulk:
post:
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/BulkOrderRequest'
examples:
bulkImport:
summary: A batch of three orders
# externalValue points at the RAW payload, not an Example Object
externalValue: './examples/v1/bulk_order.json'
responses:
'202':
description: Batch accepted for processing
content:
application/json:
examples:
accepted:
$ref: '#/components/examples/BulkAccepted'
components:
examples:
BulkAccepted: # a reusable Example Object
summary: Standard acceptance receipt
externalValue: './examples/v1/bulk_order_response.json'
The distinction trips people up constantly: putting a raw JSON file behind $ref makes a renderer expect Example Object keys (value, summary, externalValue) and fail, while putting an Example Object behind externalValue double-wraps it. Use externalValue for the raw sample, and $ref to reuse a named Example Object you defined under components/examples. This keeps the root spec focused on routing and schemas, consistent with structuring OpenAPI paths. Confirm everything resolves with a bundle:
npx @redocly/cli bundle openapi.yaml --output dist/openapi-bundled.yaml
Integration Pattern
Enforce two things on every pull request: that responses actually carry examples, and that those examples resolve. The Spectral rule asserts presence; the workflow bundles to validate resolution and runs ajv to catch schema drift.
# .spectral.yaml
extends: ["spectral:oas"]
rules:
response-has-example:
description: 'JSON responses must include at least one example.'
severity: error
given: "$.paths[*].responses[*].content['application/json']"
then:
field: examples
function: truthy
# .github/workflows/spec-validation.yml
name: Validate & Bundle Spec
on:
pull_request:
paths: ['openapi.yaml', 'examples/**', '.spectral.yaml']
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- name: Lint (enforces example presence)
run: >
npx @stoplight/spectral-cli lint openapi.yaml
--ruleset .spectral.yaml --fail-severity error
- name: Bundle (validates $ref / externalValue resolution)
run: npx @redocly/cli bundle openapi.yaml --output dist/openapi-bundled.yaml
- name: Validate examples against schemas (drift check)
run: |
npx @redocly/cli bundle openapi.yaml \
--output dist/openapi-bundled.yaml
npx ajv validate -c ajv-formats \
-s dist/schemas/BulkOrderRequest.json \
-d "examples/v1/bulk_order.json" --all-errors
The lint step blocks merges when a response lacks an example. The bundle step fails loudly if any $ref or externalValue cannot be resolved — the most common CI break, because runners execute from a different working directory than your laptop. The ajv step is the drift guard: it validates each raw example against the schema it claims to illustrate, catching the case where a schema gained a required field but the example was never updated. Treat the same “examples are part of the contract” mindset described in defining JSON Schema components and the breakdown in managing large example payloads in API specs.
Advanced Options
Version-aware portal rendering
Map semantic version tags to dedicated example directories and point each version’s portal page at the matching bundled output. This guarantees the try-it-out console pre-fills with samples that match the spec the consumer is reading:
<!-- v2 portal page -->
<redoc spec-url="/dist/v2/openapi-bundled.yaml"></redoc>
Because examples/v1/ and examples/v2/ are separate trees, a v2-only field can appear in v2 samples without ever touching v1 documentation, and the diff for a version bump stays contained to one directory.
Multiple named variants per media type
Use the plural examples to document edge cases side by side — a minimal request, a maximal request, and an error case — each with a summary the portal surfaces in a dropdown:
examples:
minimal:
summary: Required fields only
externalValue: './examples/v1/order_minimal.json'
fullyPopulated:
summary: Every optional field set
externalValue: './examples/v1/order_full.json'
AsyncAPI message examples
Aligning with AsyncAPI event-driven patterns, externalize message samples the same way. AsyncAPI’s examples is an array of objects, so reference each entry’s payload file:
components:
messages:
LargeOrderEvent:
payload:
$ref: '#/components/schemas/LargeOrder'
examples:
- name: bulkBatch
payload:
$ref: './examples/large_order_event.json'
Verification & Testing
Run the full local sweep before opening a PR so resolution and drift problems surface on your machine rather than in CI.
# 1. Presence: every response carries an example
npx @stoplight/spectral-cli lint openapi.yaml --ruleset .spectral.yaml
# 2. Resolution: every $ref / externalValue resolves
npx @redocly/cli bundle openapi.yaml --output dist/openapi-bundled.yaml
# 3. Drift: each external example still validates against its schema
npx ajv validate -c ajv-formats \
-s dist/schemas/BulkOrderRequest.json \
-d examples/v1/bulk_order.json --all-errors
# 4. Render smoke test: confirm the portal builds with resolved examples
npx @redocly/cli build-docs dist/openapi-bundled.yaml -o /tmp/docs.html
A clean lint confirms coverage, a clean bundle confirms resolution, a clean ajv run confirms no drift, and a successful docs build confirms the examples actually render in the portal. The fourth step matters because an example can validate against its schema yet still break a renderer if it is wrapped incorrectly.
Troubleshooting
Inline example bloat slows the parser and causes merge conflicts
Large JSON embedded directly in the spec inflates file size and collides on nearly every edit. Externalize any example over roughly 50 lines into examples/ and reference it with externalValue. The spec then changes only when the reference changes, not when sample data does.
Schema–example drift breaks the try-it-out console
When a schema gains or renames a field but the example is not updated, the console pre-fills invalid data and misleads consumers. Add the ajv validation step to CI so each external example is checked against its parent schema on every PR, failing the build on mismatch.
Broken $ref / externalValue paths in CI but not locally
CI runners start from a different working directory, so paths that resolve on your machine fail in the pipeline. Always use paths relative to the referencing file, never absolute local paths, and run redocly bundle in CI to catch resolution failures with the exact offending pointer.
externalValue rendered as raw text instead of a payload
This happens when a raw file is placed behind $ref (which expects an Example Object) or an Example Object is placed behind externalValue. Use externalValue for raw payload files and $ref for named Example Objects under components/examples; do not mix them for a single example.
FAQ
Should I use example or examples in OpenAPI 3.1?
Use examples (plural) to provide multiple named variants such as edge cases or versioned samples. The singular example is still valid but holds only one value per media type and cannot carry a summary or description.
What is the difference between $ref and externalValue for examples?
Use $ref to pull in an Example Object defined elsewhere in the document or a fragment file, which the bundler inlines. Use externalValue to point at a raw payload file by URI when the content is not itself an Example Object, which keeps large or binary samples out of the spec.
How do I validate external example files against schemas in CI?
Run ajv-cli to validate each external JSON file against its compiled schema, or rely on redocly lint which checks example values against their parent schema. Wire either into the pull request workflow so drift fails the build.
Can AsyncAPI handle large message examples efficiently?
Yes, externalize payloads via $ref in the message examples array and keep the heavy sample data in separate files. The AsyncAPI Generator resolves them on demand so the source document stays small and diff-friendly.
Related
- OpenAPI & AsyncAPI Schema Authoring — the parent overview for contract-first authoring.
- Managing large example payloads in API specs — scaling externalized examples to big datasets.
- Defining JSON Schema components — the schemas your examples illustrate.
- Structuring OpenAPI paths — keep the root spec focused on routing.
- AsyncAPI event-driven patterns — externalize message payload examples.