Asking for JSON versus guaranteeing it
By the end: You can make a model return data your code can parse every time.
- Why asking in the prompt is not enough
- Schema-constrained output
- What it guarantees and what it does not
- Handling the failure that remains
New to structured output? Often you do not want prose back, you want data your program can use: named fields with values, of the kind JSON holds. Structured output is how you ask for that and, when the API supports a schema, how you enforce it — you hand over the shape you expect and the model is prevented from producing anything that would not fit, so what arrives always parses. Why it matters
If your code has to parse what a model writes, you need the shape of that output to be certain rather than likely. This section is about the gap between requesting a format and enforcing one, and about what schema-constrained output does and does not promise you. It is the difference between a job that runs unattended and one that breaks every few days in a way nobody can reproduce.
The 95 percent problem#
You ask for JSON in the prompt. It works, most of the time. Then occasionally the model wraps it in a code fence, or opens with 'Here is the JSON:', or leaves a trailing comma. Your parser raises, and it happens often enough to matter and rarely enough to be hard to reproduce.
What you asked for:
{"name": "Jack", "role": "engineer"}
What sometimes arrives:
Here is the JSON you requested:
```json
{"name": "Jack", "role": "engineer"}
```An instruction in a prompt is a request. The model usually honours it, and sampling means usually is not always.
Constrained decoding#
Most providers can take a schema and restrict which tokens are allowed to be generated at each step. If the schema says the next thing must be a closing brace, no other token can be sampled. The output cannot be malformed, because malformed output was never reachable.
schema = {
"type": "OBJECT",
"properties": {
"name": {"type": "STRING"},
"amount": {"type": "NUMBER"},
"overdue": {"type": "BOOLEAN"},
},
"required": ["name", "amount", "overdue"],
}
response = client.generate(
prompt=text,
response_mime_type="application/json",
response_schema=schema,
)
data = json.loads(response.text) # this cannot raise a format errorTypes are enforced too. An amount declared as a number comes back as 1240.5, not the string '1,240.50'. That alone removes a layer of cleanup code.
What it does not fix#
Valid shape, invented content
A schema guarantees structure and nothing else. Ask for a required amount when the source document has no amount, and you get perfectly formed JSON containing a number the model made up. Nothing in the response tells you it was invented.
Two things follow. Make fields optional when they genuinely might be absent, and say in the prompt that null is the right answer for a missing value. Then verify the values themselves, which is what evaluation is for.
# Forces invention when the field is missing from the source
"required": ["name", "amount", "date"]
# Lets the model tell the truth
"required": ["name"]
# plus, in the prompt:
# "Use null for any field not present in the text. Do not guess."The failure that remains#
A schema does not stop you running out of tokens. If generation hits the limit halfway through, you get half an object, which is not valid JSON. Check the stop reason before parsing.
if response.stop_reason == "max_tokens":
raise ValueError("truncated before the JSON was complete")
data = json.loads(response.text)Where people go wrong
Marking everything required. It feels safer and it guarantees hallucination on any document that is missing a field.
If this comes up in an interview
Your extraction pipeline returns malformed JSON 5 percent of the time. What are the likely causes?
Two. Format drift from sampling, which shows as code fences or preamble, fixed by constrained decoding or a lower temperature. And truncation from hitting the token limit, which gives you half an object. The stop reason tells you which one you have.
Then they push further
You switch to a response schema and it still fails sometimes. Why?
Common reasons: the output hit max_tokens and was cut off, the schema uses features the provider does not enforce, or the JSON is valid but the values are wrong. Check the stop reason first.
Valid JSON, wrong values. How do you catch that?
Validate beyond the schema: ranges, consistency between fields, and that extracted values actually appear in the source where they should. Count failures by type so you know which to fix first.
What gets marked up. Marked up for checking the stop reason for truncation, and for separating well-formed output from correct output.
What does a response schema guarantee?
That the output is structurally valid and matches the declared types. It guarantees nothing about whether the values are correct. A schema will happily return well-formed JSON full of invented data.
How does constrained decoding actually work?
At each generation step the sampler is restricted to tokens that keep the output valid against the schema. Invalid continuations are given zero probability, so malformed output is not reachable rather than merely discouraged.
Should every field be required?
No. Marking a field required when the source may not contain it forces the model to invent a value. Make genuinely optional fields optional and instruct the model to return null rather than guess.
Build this: Extract invoice fields from a document with a schema. Then delete the amount from the source text and run it again with amount required. Watch what number comes back, then make it optional and compare.
Watch it explained
This is one section of Structured output, 10 hours across 3 sections. Read it in full to get the whole topic in order.
Stop 11 of 50 on the AI Engineer route
0 of 50 topics done · 0h behind you, 603h to go
What you have marked done is stored in this browser only. It does not reach a server, and clearing site data will clear it.