Your spec has every operation and half the answers. POST /meters returns 201 with an empty
application/json object under content, the 404 has a description and no body, and the one
schema that exists carries no example. A generator makes an untyped client from it, a mock server
has nothing to serve, and the converter that wrote the file never saw a real response.
What you get
You will end up with a ruleset that lists every missing schema and example, a prompt that hands the gaps and fixtures to an agent, and two validators for the result. This is for you if you own a hand-written or converted spec whose responses are empty.
Short answer
Write two Spectral rules that fail a response media type with no schema and one with no
example, and run them beside the built-in oas3-valid-media-example. Hand the agent the
findings and the captured payloads under fixtures/, and ask it to add each schema under
components and reference it. Run Spectral and openapi-examples-validator after every batch,
because the first batch tends to carry an example its own schema rejects.
You will need
Node 22 or later, a spec whose operations exist, and response payloads captured from a test suite
or a proxy, one file per response. Verified 2026-09-25 against Node 22.22.2,
@stoplight/spectral-cli 6.16.3, openapi-examples-validator 7.1.0 and quicktype 26.0.0. The
Media Type Object is where a
response’s schema and example live, and the
Components Object is where a
schema goes when two responses share it.
Approaches compared
| Approach | When it fits | What it costs you | When to pick something else |
|---|---|---|---|
| Claude Code | Dozens of gaps, payloads on disk, and shared components wanted rather than inline copies | A guessed format or enum member in the first batch, and a validation run you have to insist on | There are no captured payloads, so every schema would be a guess |
openapi-examples-validator | A second opinion on every example from a tool with one job | Another dependency for a check Spectral also performs, and errors on stderr rather than a report | One checker in CI is enough and it is already Spectral |
| quicktype | One payload, one schema, and no judgement wanted | A schema per sample with no reuse, every key required, and a null typed as null | The same object appears in three responses and should be one component |
| Spectral | Listing the gaps, then gating the result in CI on every commit | Two custom rules to write, and a ruleset that says where the holes are and fills none of them | The examples need writing, not counting |
| Stoplight Studio | A handful of gaps and a person who knows the payloads | A form per schema filled by hand, so nothing is guessed and nothing is fast | The gaps run into the dozens, or the payloads already exist as files |
The five are not rivals for the same job. Spectral finds the gaps and gates the result. The agent,
quicktype and Studio are three ways to fill them, from most judgement to least, and the validator
is the check that the filling was right. The agent is the only one of the three that will reuse a
component, and the only one that will invent an enum member.
List the gaps before anyone fills them
The Spectral OpenAPI ruleset knows nothing about missing schemas, because a response with no
content is valid OpenAPI. Two custom rules say what this spec requires, and the rest of
spectral:oas is off so the run reports gaps and nothing else.
extends: [[spectral:oas, off]]
rules:
oas3-schema: error
oas3-valid-media-example: error
oas3-valid-schema-example: error
response-has-content:
description: every response except 204 declares a content object
given: "$.paths[*][*].responses[?(@property != '204')]"
severity: error
then:
field: content
function: truthy
response-content-has-schema:
description: every response media type carries a schema
given: "$.paths[*][*].responses[*].content[*]"
severity: error
then:
field: schema
function: truthy
response-content-has-example:
description: every response media type carries an example or examples
given: "$.paths[*][*].responses[*].content[*]"
severity: error
then:
function: schema
functionOptions:
schema:
type: object
anyOf:
- required: [example]
- required: [examples]
truthy and schema are two of the
core functions. The
third rule uses schema because a media type may carry either example or examples, and a
truthy check on one field cannot say “one of these two.”
oas3-valid-media-example
is the built-in rule that will matter after the agent has written something.
npx spectral lint openapi.yaml
16:15 error response-has-content every response except 204 declares a content object paths./meters.get.responses[200]
31:30 error response-content-has-example every response media type carries an example or examples paths./meters.post.responses[201].content.application/json
31:30 error response-content-has-schema every response media type carries a schema paths./meters.post.responses[201].content.application/json
35:38 error response-content-has-example every response media type carries an example or examples paths./meters.post.responses[422].content.application/problem+json
35:38 error response-content-has-schema every response media type carries a schema paths./meters.post.responses[422].content.application/problem+json
50:30 error response-content-has-example every response media type carries an example or examples paths./meters/{meter_id}.get.responses[200].content.application/json
53:15 error response-has-content every response except 204 declares a content object paths./meters/{meter_id}.get.responses[404]
82:30 error response-content-has-example every response media type carries an example or examples paths./meters/{meter_id}/readings.post.responses[201].content.application/json
82:30 error response-content-has-schema every response media type carries a schema paths./meters/{meter_id}/readings.post.responses[201].content.application/json
✖ 9 problems (9 errors, 0 warnings, 0 infos, 0 hints)
All nine findings, spread across four operations, carry a JSON path. That list is the work order, and it is what the agent gets, rather than “fill in the spec.”
Hand the agent the gaps and the fixtures
The prompt names the command that lists the gaps, the directory of payloads, the rule about components, and the two checks that end the loop. Everything the agent might otherwise guess is either on disk or forbidden.
Fill the gaps that `npx spectral lint openapi.yaml` reports in openapi.yaml.
Rules:
1. Every response media type gets a `schema` and an `example`. Derive both from
the payloads under fixtures/: meter.json, meter-list.json, reading.json,
problem-404.json and problem-422.json are real responses captured from the
test suite.
2. Put every object schema under components/schemas and reference it with
`$ref`. Do not inline a schema that a second response could share. The
error responses share one schema, `Problem`, shaped like RFC 9457.
3. Keep the examples exactly as the fixtures have them. Do not invent values,
shorten strings, or change an enum member.
4. After each batch of edits, run both checks and stop when both pass:
`npx spectral lint openapi.yaml` and
`npx openapi-examples-validator openapi.yaml`.
5. Write the result to openapi.yaml in place. Do not touch fixtures/.
In Claude Code, @fixtures/meter.json in the message puts the file’s content in the
conversation. The
file reference
documentation adds that a directory reference gives a listing and not the contents, so the prompt
names each file. Rule 3 exists because an example is evidence, and an agent that tidies
mtr_01HZX4 into meter-1 has replaced evidence with an opinion.
No model is called by anything on this page. openapi.batch1.yaml and openapi.filled.yaml
were written for it as recorded stand-ins for the agent’s first and second batch. They have the
shape an agent produces and the two defects a first batch tends to carry, so every check below
runs offline and prints the same thing every time.
Validate after the first batch, not at the end
The first batch closes all nine gaps. Every response has a schema, every schema is a $ref, and
every media type has an example. The gap rules are silent, and the built-in example rule is not.
npx spectral lint openapi.batch1.yaml
30:27 error oas3-valid-media-example "kind" property must be equal to one of the allowed values: "electricity", "gas", "water" paths./meters.get.responses[200].content.application/json.example.items[1].kind
53:31 error oas3-valid-media-example "installed_at" property must match format "date-time" paths./meters.post.responses[201].content.application/json.example.installed_at
Two defects, both of a kind that reads correctly at a glance. The list example says kind: elec
where the enum the same batch wrote says electricity, and the create example says
installed_at: '2026-03-02' against a schema that says format: date-time.1 The second
checker reports the same two, as an array on stderr.
npx openapi-examples-validator openapi.batch1.yaml
Validating examples
Schemas with examples found: 4
Examples without schema found: 0
Total examples found: 6
Errors found.
[
{
"type": "Validation",
"message": "must match format \"date-time\"",
"instancePath": "/installed_at",
"schemaPath": "#/properties/installed_at/format",
"keyword": "format",
"params": {
"format": "date-time"
},
"examplePath": "/paths/~1meters/post/responses/201/content/application~1json/example"
},
Run both after every batch, not once at the end. An agent that gets the second batch’s findings fixes two values. An agent that gets thirty findings after ten batches starts rewriting schemas to fit the examples. That is the wrong direction: the examples came from the fixtures, and the schemas were the guess.
Compare with what quicktype infers
quicktype answers the same question with no model and no judgement: one sample in, one JSON Schema out.
npx quicktype -l schema fixtures/meter-list.json
"next_cursor": {
"type": "null"
}
},
"required": [
"items",
"next_cursor"
],
"title": "MeterList"
},
"Item": {
"type": "object",
"additionalProperties": false,
"properties": {
"id": {
"type": "string"
},
"site": {
"type": "string"
},
"kind": {
"type": "string"
},
"installed_at": {
"type": "string",
"format": "date-time"
}
},
"required": [
"id",
"installed_at",
"kind",
"site"
],
"title": "Item"
}
Three things in that output are the cost of having no judgement. next_cursor is typed null,
because the one sample had a null in it, so a schema derived this way rejects the second page.
The array element is named Item after the property, not Meter, and running the tool on
meter.json produces a second, identical schema called Meter, so nothing is shared. And
additionalProperties is false with every key required, which turns the first response that
adds a field into a validation failure.2 What quicktype gets right, it gets right every time:
the date-time format on installed_at was inferred from the value, and the agent’s first batch
got that one wrong.
Check it worked
The second batch changes two values and nothing else. Both checkers go quiet, and the test pins the counts on all three documents so a regenerated ruleset or a changed fixture fails here.
npx spectral lint openapi.filled.yaml
No results with a severity of 'error' found!
npx openapi-examples-validator openapi.filled.yaml
Validating examples
Schemas with examples found: 4
Examples without schema found: 0
Total examples found: 6
No errors found.
test('batch one closes every gap and fails example validation twice', () => {
const { code, results } = spectral('openapi.batch1.yaml')
assert.equal(code, 1)
assert.deepEqual(results.map((r) => r.code), ['oas3-valid-media-example', 'oas3-valid-media-example'])
assert.deepEqual(results.map((r) => r.path.at(-1)), ['kind', 'installed_at'])
const v = validator('openapi.batch1.yaml')
assert.equal(v.code, 1)
assert.deepEqual(v.errors.map((e) => e.keyword).sort(), ['enum', 'format'])
})
node --test gaps.test.mjs
1..5
# tests 5
# suites 0
# pass 5
# fail 0
# cancelled 0
# skipped 0
# todo 0
The fourth test reads the filled document and asserts every response schema is a $ref, and the
fifth that every example is its fixture, verbatim. Those are the two rules in the prompt that no
validator checks. A schema inlined three times validates three times, and so does a list example
with one of its two meters dropped.
When it goes wrong
The gap rules pass, but the client is still untyped. The agent inlined the schemas, so every
response validates and nothing is shared. Add the $ref check to the ruleset as a fourth rule,
with field: schema.$ref and resolved: false, rather than trusting the prompt. Spectral
resolves every $ref before a rule runs, so without that flag the rule fails every response
that got it right.
The example passes Spectral and fails in production. The example was validated against a schema the agent also wrote, so both agree and both are wrong. Keep the examples verbatim from captured payloads, which is rule 3, and validate the payloads against the schema with the second checker rather than the other way round.
A date passes one validator and fails the other. JSON Schema treats format as an annotation
unless a validator is told otherwise, and different tools decide differently. Both tools here
enforce date-time, which is why the first batch was caught; a third tool may not, so run the two
that do.
The agent rewrites the Problem schema to fit an example. The prompt said the error responses
share one schema shaped like RFC 9457,3 and a
fixture carried a detail the agent’s first draft left out. Adding the optional property is right;
dropping the $ref to give one response its own copy is the failure to reject in review.
When not to do this
Do not hand an agent a spec with no fixtures and ask it to fill the schemas. Without a captured payload, every property name, every type and every format is a guess, and a guessed schema that validates its own guessed example proves nothing. Capture responses first, from tests or a proxy, and then fill.
Do not run the example check once, at the end. Ten batches of unvalidated edits produce a document where the examples and the schemas have drifted from each other in both directions, and the fix becomes a rewrite rather than two values.
Do not accept a schema the agent inlined because the gap rule went green. The rule asks whether a schema exists, not where. An inline copy under three responses is three schemas to keep in step, and the next agent to work on the file will edit one of them.
Do not let the quicktype output into components unedited. Its schemas are faithful to one sample:
a null becomes a null type, every key becomes required, and unknown keys are forbidden. It is a
draft for a person to edit, and as a draft it is faster than any agent.
Related how-tos
Last verified
Verified 2026-09-25 against Node 22.22.2, @stoplight/spectral-cli 6.16.3,
openapi-examples-validator 7.1.0 and quicktype 26.0.0. Every output block is what the command
preceding it printed, run in the page’s code directory after npm ci. The two filled documents
are committed stand-ins for an agent’s batches and were not produced by a model.
Footnotes
-
The two checkers agree on
2026-03-02because both chose to. JSON Schema 2020-12 says theformatkeyword is collected as an annotation, that an implementation may also treat it as an assertion, and that such evaluation must be switched off by default. It goes on to allow an implementation to validate any format as a no-op that always returns true. The same paragraph remarks that this matches the reality of implementations, which provide widely varying levels of validation. A date-time check is therefore something a validator opts into on your behalf, and two of them did. ↩︎ Back to text -
quicktype writes
"$schema": "http://json-schema.org/draft-06/schema#". The draft-06 release notes open by noting that draft-07 has been released, then list what draft-06 changed from draft-04.idbecame$id,$refbecame legal only where a schema is expected, andexclusiveMinimumturned from a boolean into a number. They also answer the question of what happened to draft-05, which was a readability rewrite whose name implementations are asked not to advertise. OpenAPI 3.1 uses 2020-12, three drafts later, and a generator reads both without complaint. ↩︎ Back to text -
RFC 9457, by Nottingham, Wilde and Dalal, was published in July 2023 and obsoletes RFC 7807. Its problem object has five members:
type,title,status,detail, andinstance. The rule for a member whose value has the wrong type is to ignore it and continue as if it were absent. A validator with a schema is stricter than the standard it validates. ↩︎ Back to text