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.

Externalized examples resolved at bundle time The spec references example files by ref, the bundler inlines them, and the portal renders matching samples. openapi.yaml $ref / externalValue examples/v1/*.json bundle portal try-it-out prefilled

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'

Examples are load-bearing, not decoration

An example looks like documentation garnish and is in fact the most consumed part of a reference. Nobody transcribes a schema table into code; everybody copies the example. It also feeds three other systems: a mock server derives its responses from it, a try-it console prefills the request body from it, and generated snippets embed it. One stale example is therefore four wrong things, only one of which is visible on the page.

What examples feed An example is copied by readers, returned by mock servers, prefilled into try-it consoles, and embedded in generated snippets. one example in the spec copied by readers returned by the mock prefilled in the console embedded in snippets one stale example breaks all four

Nothing checks them by default. An OpenAPI example is free-form data sitting beside a schema, and no validator compares the two unless you ask it to — so an example decays silently from the moment the schema next changes, and the first person to notice is a reader who copied it and received a validation error for following the documentation exactly.

Size, and where large payloads should live

The other recurring problem is bulk. A response that legitimately returns a hundred fields produces an example that dominates the page, pushes the schema table below the fold, and makes the specification file unpleasant to review. The instinct to trim it into something unrepresentative is worse than the problem, because a truncated example that looks complete teaches the wrong shape.

Handling large examples A representative subset shown inline with the full payload referenced externally keeps the page readable without misrepresenting the shape. full payload inline accurate dominates the page schema table pushed away silently truncated short and readable looks complete, is not teaches the wrong shape subset + external link representative inline full payload one click away honest and readable

The workable middle is an inline example that is genuinely representative — every distinct field type, one entry per array rather than forty — with the complete payload available as a referenced file for anyone who needs it. What makes that honest rather than misleading is saying so: a one-line note that the inline version is abridged and the full payload is linked costs nothing and prevents a reader inferring that an array always has one element.

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.

Naming examples so they can be selected

A single unnamed example per response covers the happy path and nothing else, which leaves the branches consumers most need to handle — the empty collection, the last page, the partially settled invoice, the error body — undocumented and unreachable. Named examples fix both problems at once: they appear as selectable options in the rendered reference, and they become addressable in a mock server, so a client can request a specific scenario rather than only the default.

The names are worth choosing deliberately because they show up in the interface. firstPage and lastPage tell a reader what they are about to see; example1 and example2 do not. A short summary alongside each one costs a line and turns a dropdown of opaque identifiers into something a reader can navigate.

Two or three named examples per operation is usually the right amount. The default case, the interesting edge case a consumer will get wrong, and the error shape they must handle cover most of the value; beyond that the selector itself becomes a thing to scroll through, and the marginal example is rarely the one anybody needed.

Keeping them true over time

Because nothing validates examples by default, the only durable answer is to add the check yourself and run it as a gate. Validating every example against the schema it sits beside is a small script and catches exactly the failure that otherwise reaches readers — an example describing a shape the API stopped returning two releases ago.

Turning that check on for the first time will fail, because every mature specification has accumulated some drift. Clear the backlog as its own piece of work before making the check required, so the first pull request to encounter it is not blocked by problems somebody else introduced. After that, every failure genuinely belongs to the change that caused it, which is the condition under which people fix them the same day.

Extend the check to request bodies and parameters as well as responses. Response examples at least get looked at when someone reads the page; a stale request-body example is the one a reader copies straight into their client, and nothing else in the pipeline is checking it.

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.

Generate the floor, author the interesting cases

The two ways of producing examples are complementary rather than competing, and using each for what it is good at removes most of the maintenance burden. Generation guarantees that no response is left showing placeholder values, because it derives a valid example from the schema for anything you have not written one for. Authoring supplies what a generator cannot invent: that an overdue invoice carries a late fee, that the final page has a null cursor, that a partially settled payment has two of its three fields populated.

Generated examples are only as good as the schema they come from, which turns out to be a useful forcing function. A field declared as a bare string produces "string"; the same field with a format, a pattern, or an enum produces something that looks real. Improving the schema to make generation useful improves the rendered reference, the validation, and any property-based testing at the same time, so the effort is never wasted on the examples alone.

Where the two meet, authored examples win — never let generation overwrite something a person wrote. Keeping generated output in a separate overlay rather than writing it into the source document makes that separation structural instead of a convention someone has to remember.

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.

Treated this way, examples stop being a maintenance burden that decays between releases and become a derived artifact with one hand-written layer on top — which is the same shape that works for changelogs, for mocks, and for most of the other things a specification can generate.

It is also the arrangement that scales, because the generated layer grows with the API while the hand-written layer stays the size of the interesting cases.

And the hand-written layer is where the actual teaching happens, which is a good use of the effort it costs.

Well-chosen examples are among the highest-return content a reference can carry.