Schema drift: why structured extraction fails quietly, and how to make it fail loudly
A teardown of the failure that produces no error: the model returns something shaped almost right, the schema accepts it, and three steps later you have a wrong answer nobody flagged.
Structured extraction rarely fails the way you prepare for. You expect malformed JSON and a parse error. What you get is well-formed JSON with a field renamed, a number arriving as a string, or a key that was not there yesterday — and everything downstream accepting it without complaint.
1. What schema drift actually is
Drift is not one event. It is the gap that opens between the shape you assumed and the shape that arrives, from either side:
- From the model. A new version phrases the same answer differently, or starts wrapping the result in one more object.
- From the prompt. Someone adds a sentence, and the output gains a field nobody asked for.
- From the source. The API you extract from renames a key or changes a type.
- From the consumer. Your own code starts needing a field the extractor was never asked to produce.
2. What it looks like in a visual flow
In a canvas, the contract between two steps is a field mapping made by hand on the day it was built. When drift happens, that mapping does not error — it resolves to empty. Three shapes recur:
| What drifted | What the run shows | What reaches the customer |
|---|---|---|
| A field was renamed | Green. The mapping returns empty. | A message with a blank where the number was, or a zero. |
| A number arrived as text | Green. Concatenation instead of arithmetic. | A total that is two numbers stuck together. |
| An extra field appeared | Green. Nothing reads it. | Nothing — until someone needs it and it was there all along, unused. |
| The model wrapped the result | Green. The mapping points one level too shallow. | Empty output, reported as a successful run. |
3. Make the shape explicit, then refuse surprises
The first move costs almost nothing: declare the shape, and say that anything else is an error. Strictness is opt-in, so opt in.
{
"type": "object",
"additionalProperties": false,
"required": ["sku", "cantidad", "precio_unitario"],
"properties": {
"sku": { "type": "string", "minLength": 1 },
"cantidad": { "type": "integer", "minimum": 1 },
"precio_unitario": { "type": "number", "exclusiveMinimum": 0 }
}
}additionalProperties: false is the line that turns silent drift into a rejection you can see. required is what turns a missing field into an error instead of an undefined.Note what the types are doing. integer rather than number for a quantity; a minimum that rules out zero and negatives; a price that cannot be zero. A type that accepts anything catches nothing — most of the value of a schema is in the constraints, not in the field names.
4. Validation gates, and what happens after one fails
A schema tells you the payload is wrong. It does not tell you what to do about it, and that decision is where systems differ:
- Reject at the boundary. Validation runs before anything acts on the data. A bad payload never reaches the code that would have used it.
- Repair once, with the error in hand. Give the model back the validation message — which field, what was expected — and ask again. This is worth doing because it is a *different* attempt, not the same one.
- Escalate, don't loop. If the second attempt fails the same way, stop. A third identical try is not error handling; it is the same gamble, paid again.
- Fall back to something deterministic. Answer from a template, or hand to a human. An answer you cannot back is worse than no answer.
5. Contract tests: the part people skip
A schema protects you at runtime. What protects you at build time is a set of golden fixtures: real payloads you have actually seen, checked against the current schema on every commit.
fixtures/
01-normal.json el caso feliz
02-campo-renombrado.json lo que llegó el día que se rompió
03-numero-como-texto.json idem
04-envuelto-de-más.json idem
05-campo-extra.json idemThe discipline is simple and it is the whole method: when something drifts in production, the fix is not complete until the payload that broke it is a fixture. Otherwise you will fix the same drift twice.
6. What to measure
We are not going to quote you our numbers here, because a resilience figure without the method behind it is decoration. What is useful is knowing which four to watch, and what each one tells you when it moves:
| Measure | What it tells you when it rises |
|---|---|
| Share of extractions rejected at the gate | Either the source drifted, or your schema is stricter than reality. Both are worth knowing; they are not the same. |
| Share repaired on the second attempt | Your repair loop is earning its cost. If it is near zero, the retry is theatre. |
| Share escalated after repair | The drift is structural, not incidental. A schema change is due. |
| Fields declared vs fields the source actually sends | Coverage. The gap is where the next silent failure will come from. |
7. Hardening checklist
- Every extraction has a declared schema, with
additionalProperties: false. - Constraints, not just types: ranges, lengths, enums where the set is known.
- Validation runs before anything acts on the data, not after.
- One repair attempt, fed the validation error. Never a blind retry.
- A deterministic fallback for when repair fails — template or human, but decided in advance.
- Golden fixtures in version control, including every payload that has broken you.
- Rejection and repair rates on a dashboard someone actually looks at.
Where the schema behaviour comes from
The default behaviour quoted above is from the JSON Schema documentation itself.
- object — By default, «providing additional properties is valid»: a schema accepts fields it never declared. Setting
additionalPropertiestofalseis what stops that — strict validation is something you ASK for, not something you get.
Checked on 2026-09-29.
Frequently asked questions
What is schema drift in LLM extraction?
The gap between the shape your code expects and the shape that actually arrives. It can come from a model version, a prompt edit, or the source API — and because JSON Schema accepts undeclared fields by default, it usually validates clean.
Should I retry when validation fails?
Once, and only if you give the model something new: the validation error itself. Resending the same prompt is not a retry, it is the same attempt repeated at the same cost.
Is strict validation enough on its own?
It turns a silent failure into a visible one, which is most of the value. What it does not do is decide what happens next — that needs a repair step, an escalation path and a deterministic fallback.
Where do contract tests fit?
They catch drift at build time instead of at 3am. Keep a fixture for every payload that has ever broken you and run them against the current schema on every commit.