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.
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
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:
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.
Related
- Example Payload Management — the parent guide on organising and sizing examples.
- Generating mock servers from OpenAPI with Prism — the mock that depends on these examples.
- Defining JSON Schema Components — constraining the schemas that drive generation.
- Spec Linting & Governance — requiring an example on every response in the first place.