Generating Examples from JSON Schema

This guide is part of Example Payload Management within OpenAPI & AsyncAPI Schema Authoring. It covers producing examples from the schema rather than by hand, and — more importantly — making sure every example in the document still matches the schema it claims to illustrate. The prompt is usually a support ticket: someone copied an example from the reference, sent it, and got a validation error back.

Everything an example feeds One example in the specification drives the rendered reference, the mock server response, the try-it console prefill and copied client code. one example in the specification rendered reference what a reader copies mock server what consumers build against try-it console prefilled request body generated snippets a stale example breaks all four

Problem & Context

An OpenAPI example is free-form data. Nothing in the specification format requires it to satisfy the schema it sits beside, and no validator checks it by default. So an example is a copy of the truth taken at one moment, and it starts decaying immediately: a field is renamed, a type is tightened, a property becomes required, and the example carries on describing the old shape with nothing to signal otherwise.

The damage is disproportionate because examples are the part readers actually use. Nobody transcribes a schema table into code; everybody copies the example. A stale one produces a 422 for a developer who did exactly what the documentation said, which is the most trust-destroying failure a reference can have.

It also propagates. Mock servers derive their responses from examples, so consumers build against the stale shape. Try-it consoles prefill request bodies from them, so the console’s default request fails. One wrong example is four wrong things.

The fix has two halves that are easy to confuse. Generation guarantees an example is valid; it cannot make it meaningful. Validation in CI guarantees the ones you wrote stay valid. You need both.

Step-by-Step Solution

1. Find the examples that already lie

Before generating anything, measure the existing drift:

npm install --save-dev ajv-cli@5 @redocly/cli@2 js-yaml
npx @redocly/cli@2 bundle openapi/api.yaml -o dist/api.bundled.yaml
node -e '
const fs=require("fs"), yaml=require("js-yaml"), Ajv=require("ajv/dist/2020");
const doc=yaml.load(fs.readFileSync("dist/api.bundled.yaml","utf8"));
const ajv=new Ajv({strict:false, validateFormats:false});
let checked=0, bad=0;
for (const [p,item] of Object.entries(doc.paths??{}))
 for (const [m,op] of Object.entries(item))
  for (const [code,res] of Object.entries(op.responses??{}))
   for (const [ct,media] of Object.entries(res.content??{})) {
     if (!media.schema) continue;
     const cases = media.examples
       ? Object.entries(media.examples).map(([n,e])=>[n,e.value])
       : (media.example !== undefined ? [["example",media.example]] : []);
     for (const [name,value] of cases) {
       checked++;
       const validate=ajv.compile(media.schema);
       if (!validate(value)) {
         bad++;
         console.log(`INVALID ${m.toUpperCase()} ${p} ${code} ${ct} [${name}]`);
         console.log("        " + ajv.errorsText(validate.errors));
       }
     }
   }
console.log(`\n${checked} example(s) checked, ${bad} invalid`);
'
INVALID GET /invoices/{id} 200 application/json [paid]
        data must have required property 'currency'
INVALID GET /invoices 200 application/json [firstPage]
        data/next_cursor must be string

14 example(s) checked, 2 invalid

Two of fourteen were wrong, and both had been wrong since a schema change three releases ago.

2. Constrain the schema so generation produces plausible values

A generator can only be as good as the schema. type: string gives "string"; a constrained field gives something that looks real:

components:
  schemas:
    Invoice:
      type: object
      required: [id, total_cents, currency, status, issued_at]
      additionalProperties: false
      properties:
        id:
          type: string
          pattern: '^inv_[0-9A-HJKMNP-TV-Z]{10}$'   # generators sample the pattern
          example: inv_01H8XKQ2M4
        total_cents:
          type: integer
          minimum: 0
          maximum: 100000000
          example: 24900
        currency:
          type: string
          enum: [EUR, GBP, USD]                     # an enum removes all guesswork
        status:
          type: string
          enum: [draft, open, paid, overdue, void]
        issued_at:
          type: string
          format: date-time                         # generators emit a real timestamp

3. Generate a baseline example per response

Write generated examples into an overlay rather than into the source document, so authored and generated content never get confused:

node -e '
const fs=require("fs"), yaml=require("js-yaml");
const {JSONSchemaFaker}=require("json-schema-faker");
JSONSchemaFaker.option({alwaysFakeOptionals:true, useExamplesValue:true, random:()=>0.5});
const doc=yaml.load(fs.readFileSync("dist/api.bundled.yaml","utf8"));
const overlay={overlay:"1.0.0", info:{title:"generated examples", version:"1.0.0"}, actions:[]};
for (const [p,item] of Object.entries(doc.paths??{}))
 for (const [m,op] of Object.entries(item))
  for (const [code,res] of Object.entries(op.responses??{}))
   for (const [ct,media] of Object.entries(res.content??{})) {
     if (!media.schema || media.example || media.examples) continue;   // never overwrite authored
     overlay.actions.push({
       target: `$.paths["${p}"].${m}.responses["${code}"].content["${ct}"]`,
       update: {examples: {generated: {summary: "Generated from the schema",
                                       value: JSONSchemaFaker.generate(media.schema)}}},
     });
   }
fs.writeFileSync("openapi/examples.overlay.yaml", yaml.dump(overlay));
console.log(`generated ${overlay.actions.length} example(s)`);
'
generated 9 example(s)

The deterministic random seed matters: without it every run produces different values and the overlay churns on every build.

4. Hand-write only the examples that teach something

A generator cannot know that an overdue invoice carries a late fee, or that the second page of results has a non-null cursor. Those are the examples worth writing:

              examples:
                paid:
                  summary: A settled invoice
                  value:
                    id: inv_01H8XKQ2M4
                    total_cents: 24900
                    currency: EUR
                    status: paid
                    issued_at: '2026-07-01T09:12:00Z'
                overdue:
                  summary: Past due, with a late fee applied
                  value:
                    id: inv_01H8XM7B3P
                    total_cents: 26400        # 24900 + 1500 late fee
                    currency: EUR
                    status: overdue
                    issued_at: '2026-05-01T09:12:00Z'

5. Validate every example in CI

Both kinds, on every change, as a required check:

      - name: Validate every example against its schema
        run: node scripts/validate-examples.js dist/api.bundled.yaml
Generated versus authored examples Generation guarantees every response has a valid example, while authored examples carry the scenarios a generator cannot invent; both are validated in CI. generated baseline every response has a valid example never a placeholder value valid, but not meaningful authored scenarios overdue invoice with a late fee second page with a cursor meaningful, and can go stale CI validates both against the schema

Complete Working Example

The validator, written to be the required CI check:

#!/usr/bin/env node
// scripts/validate-examples.js — every example must satisfy its own schema.
const fs = require('fs');
const yaml = require('js-yaml');
const Ajv = require('ajv/dist/2020');
const addFormats = require('ajv-formats');

const specPath = process.argv[2] || 'dist/api.bundled.yaml';
const doc = yaml.load(fs.readFileSync(specPath, 'utf8'));

const ajv = new Ajv({ strict: false, allErrors: true });
addFormats(ajv);           // so format: date-time is actually enforced

let checked = 0;
const failures = [];

function collect(media) {
  if (media.examples) {
    return Object.entries(media.examples).map(([name, e]) => [name, e.value]);
  }
  return media.example !== undefined ? [['example', media.example]] : [];
}

function check(where, schema, cases) {
  if (!schema) return;
  let validate;
  try {
    validate = ajv.compile(schema);
  } catch (err) {
    failures.push(`${where}: schema itself is invalid — ${err.message}`);
    return;
  }
  for (const [name, value] of cases) {
    checked++;
    if (!validate(value)) {
      failures.push(`${where} [${name}]: ${ajv.errorsText(validate.errors, { separator: '; ' })}`);
    }
  }
}

for (const [p, item] of Object.entries(doc.paths ?? {})) {
  for (const [m, op] of Object.entries(item)) {
    if (!['get', 'put', 'post', 'delete', 'patch'].includes(m)) continue;

    // Request bodies — the ones readers copy most often.
    for (const [ct, media] of Object.entries(op.requestBody?.content ?? {})) {
      check(`${m.toUpperCase()} ${p} request ${ct}`, media.schema, collect(media));
    }
    // Responses.
    for (const [code, res] of Object.entries(op.responses ?? {})) {
      for (const [ct, media] of Object.entries(res.content ?? {})) {
        check(`${m.toUpperCase()} ${p} ${code} ${ct}`, media.schema, collect(media));
      }
    }
    // Parameter examples drift just as easily and are never noticed.
    for (const param of op.parameters ?? []) {
      const cases = param.example !== undefined ? [['example', param.example]] : [];
      check(`${m.toUpperCase()} ${p} param ${param.name}`, param.schema, cases);
    }
  }
}

if (failures.length) {
  console.error(`${failures.length} invalid example(s) of ${checked} checked:\n`);
  failures.forEach((f) => console.error('  ' + f));
  process.exit(1);
}
console.log(`OK   ${checked} example(s) valid against their schemas`);

Expected output:

OK   23 example(s) valid against their schemas

Checking request bodies and parameters, not only responses, is what makes this worth running. 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 it is checked by nothing else in the pipeline.

Gotchas & Edge Cases

format is advisory unless you enable it. By default most validators ignore format: date-time, so an example with "issued_at": "yesterday" passes. Add the formats plugin, as the script does, or the check silently misses an entire class of error.

Generated examples churn the diff. Without a fixed random seed, regenerating produces different values every run and every build shows a diff. Seed the generator and commit the overlay, or generate at build time and never commit it — either is fine, mixing them is not.

additionalProperties: false turns a harmless example into a failure. A generated example containing a field the schema does not declare fails validation, which is correct and useful: it means the schema and the example disagree about the shape, and one of them is wrong.

Examples in a shared component are validated once but rendered everywhere. A component-level example inherited by six operations is right for some and misleading for others. Prefer examples at the media-type level where the context is known.

Turning the check on for the first time will fail the build. Every mature specification has a handful of examples that stopped matching their schemas some time ago. Clear that backlog as its own piece of work before making the check required, so the first pull request to encounter it is not blocked by drift somebody else introduced two years earlier.

A valid example can still be a bad example. "total_cents": 0 on an invoice passes every check and teaches nothing. Validation is a floor, not a standard — it stops you shipping something wrong, it does not make what you ship useful.

The two halves guarantee different things, and neither substitutes for the other:

Two different guarantees Generation guarantees an example exists and is valid, while validation guarantees the examples you wrote stay valid as the schema changes. generation guarantees an example exists everywhere coverage validation guarantees it still matches the schema correctness over time

FAQ

Should every example be generated?

No. Generate the baseline so no response is left with placeholder values, and hand-write the two or three examples per resource that show a real scenario. Generation guarantees validity; authoring conveys meaning, and readers need both.

Why do my examples drift from the schema?

Because nothing checks them. An example is free-form data in the document, so a schema change leaves it stale with no error anywhere — until a reader copies it and receives a 422 for following your documentation exactly.

Do examples affect anything other than the rendered docs?

Yes. Mock servers derive their responses from them, try-it consoles prefill request bodies from them, and some SDK documentation embeds them. One bad example propagates into all three.