Structured Outputs: Getting Valid JSON From an LLM Every Time

Getting a model to return data your code can parse used to be an exercise in defensive programming β€” prompt carefully, hope, then write a repair function for the cases where hope failed. That is no longer necessary. Constrained decoding makes invalid output structurally impossible. This page covers how that works, what it still cannot promise, and how to design a schema the model handles well.

The three approaches

Prompting for JSON β€” unreliable, fails a few percent of the time in ways that are hard to predict. JSON mode β€” guarantees it parses, not that it has your fields. Structured outputs with a schema β€” guarantees shape and types. Use the third whenever it is available.

Why prompting alone fails

A model generates one token at a time, each sampled from a probability distribution. Nothing in that process is aware of JSON grammar β€” the model produces what looked likely given its training, and JSON in training data is overwhelmingly presented inside markdown code fences.

FailureWhat you get
Markdown fences```json\n{…}\n```
PreambleHere is the JSON you requested:
Trailing commentaryNote that the second field was inferred.
Trailing comma{"a": 1, "b": 2,}
Single quotes{'a': 1}
Unescaped quotes{"note": "she said "yes""}
Comments{"a": 1 // the count}
Wrong field names{"total_amount"} when you asked for total
TruncationOutput stops mid-object

Each is individually rare. Across a production workload they are collectively common, and the failure rate rises exactly when you least want it to β€” on unusual inputs, long outputs and edge cases.

How constrained decoding works

The mechanism is simple and worth understanding, because it explains precisely what the guarantee covers.

At each step the model produces a probability for every token in its vocabulary. Normally the decoder samples from that distribution. With constrained decoding, a mask is applied first: every token that would make the output invalid against the grammar is set to zero probability.

// Generating {"name": "Acme", "count": 3} // After: {"name": "Acme" Grammar state: "inside an object, after a complete pair" Valid next tokens: , } Masked out: everything else // The model's preference among valid options is respected. // Its preference for invalid ones is unreachable.

The key property: invalid output is not detected and corrected β€” it is never generated. There is no retry, no repair, no post-processing. The token that would have opened a markdown fence had zero probability at that position.

πŸ’‘ Why this needs provider support

Masking happens inside the sampling loop, so it requires access to the logits before a token is chosen. That is why structured outputs are a provider feature rather than something a client library can add β€” and why self-hosted stacks use libraries that hook the sampler directly.

The schema is compiled into a state machine once, then consulted at every step. The runtime cost is small; the compilation happens on first use, which is why an unusual schema can add latency to its first request.

JSON mode versus structured outputs

JSON modeStructured outputs
Output parsesGuaranteedGuaranteed
Field names correctNoGuaranteed
Types correctNoGuaranteed
Required fields presentNoGuaranteed
No extra fieldsNoGuaranteed with additionalProperties: false
Values are correctNoNo

That last row is the one to internalise. Everything these features guarantee is structural. A schema requiring {"invoice_total": number} will produce a number. Whether it is the right number is entirely unaddressed.

🚨 Structural validity can disguise nonsense

Before structured outputs, a model unable to answer might produce malformed JSON or a refusal β€” visibly broken, easy to catch. Under constraint it must produce something matching the schema, so it produces a well-formed object with invented values.

The failure moved from loud to silent. Always include a way for the model to signal uncertainty β€” a nullable field, a confidence value, or an explicit could_not_determine option in an enum β€” otherwise you are forcing it to guess and formatting the guess neatly.

Designing a schema the model handles well

Constrained decoding guarantees conformance. It does not make a badly designed schema easy to fill correctly.

Keep it flat

// Harder β€” the model must hold nested state { "invoice": { "parties": { "vendor": { "details": { "name": "…" } } } } } // Easier β€” and just as usable { "vendor_name": "…", "vendor_address": "…", "invoice_total": 0 }

Reshape into your own nested structure after parsing. Extraction accuracy is what the model controls; object shape is what your code controls.

Use enums wherever the answer is closed

// Weak β€” invites free-form variation { "category": { "type": "string" } } // Strong β€” only these tokens can be generated { "category": { "type": "string", "enum": ["billing", "technical", "account", "other"] } }

An enum eliminates a whole class of downstream normalisation. Without it you will receive "Billing", "billing issue" and "BILLING" across a large enough sample. Always include a catch-all option so the model is not forced into a wrong category.

Put descriptions in the schema

{ "due_date": { "type": "string", "description": "Payment due date in YYYY-MM-DD format. Use the explicit due date if stated; otherwise compute it from the invoice date plus the payment terms. Null if neither is present." } }

Descriptions are part of the prompt the model sees, and they are frequently more effective than the same instruction in the system message because they sit adjacent to the field being generated. Handling rules for ambiguous cases belong here.

Order fields so reasoning comes first

This is the highest-leverage trick in schema design, and it follows from how generation works.

A model generates left to right and cannot revise. If the first field is the answer, it commits before any reasoning. If a reasoning field comes first, the reasoning is in context when the answer is generated.

// Weaker β€” commits immediately { "is_fraudulent": true, "reasoning": "…justification written after the fact…" } // Stronger β€” reasons, then concludes { "evidence": "Transaction is 40Γ— the account average and originates from a country with no prior activity.", "is_fraudulent": true, "confidence": 0.85 }

The improvement on classification and judgement tasks is consistent and often substantial. Note that JSON object key order is not semantically meaningful to a parser but is meaningful to generation β€” the model fills fields in the order the schema presents them.

Make uncertainty expressible

{ "vat_number": { "type": ["string", "null"], "description": "VAT number as printed. Null if not present on the document β€” do not infer or construct one." } }

Without the null option and the explicit instruction, a model asked for a VAT number on a document that has none will produce something VAT-number-shaped. It has no other legal move.

What the constraint cannot do

GuaranteedNot guaranteed
Parses as JSONValues are accurate
Field names matchThe right field was chosen
Types matchNumbers are in a sensible range
Required fields presentThey were populated meaningfully
Enum values are from the listThe correct one was picked
Strings are stringsDates are valid dates

JSON Schema's format, pattern, minimum and maximum keywords are also frequently unenforced by structured output implementations β€” the grammar covers structure and types, not value constraints. Validate after parsing regardless:

const parsed = JSON.parse(response); // Structure is guaranteed. Semantics are not. if (!/^\d{4}-\d{2}-\d{2}$/.test(parsed.due_date)) reject(); if (parsed.invoice_total < 0) reject(); if (parsed.invoice_total > 1e7) flagForReview();

Failures that still happen

Truncation

The most common remaining failure. Constrained decoding guarantees validity of what it emits, not that it will finish. Hit max_tokens mid-object and you get an incomplete, unparseable fragment.

Set the limit generously, and check the finish reason rather than only catching parse errors:

if (response.stop_reason === 'max_tokens') { // Do not attempt to parse or repair β€” the data is missing. // Retry with a higher limit, or split the task. }

Unbounded arrays

An array with no maxItems is an invitation to generate until the budget runs out. Bound them, and where the real count could be large, paginate the task instead.

Refusals

A model declining to answer must still produce schema-conforming output, so the refusal appears inside your fields β€” a name field containing "I cannot extract personal information from this document". Check for this explicitly rather than trusting that a populated field means a real value.

Streaming structured output

Structured output can be streamed, and each chunk is a fragment of JSON rather than a complete object. Rendering progressively requires a parser that tolerates incomplete input.

// Partial JSON arriving over a stream {"summary": "The contract term {"summary": "The contract term is 24 months {"summary": "The contract term is 24 months", "risk // A partial parser closes open structures speculatively // so the UI can render what has arrived so far.

Ordering the schema so user-visible fields come first pays off here: put the summary before the metadata and something useful appears immediately rather than after the whole object completes.

A working checklist

  1. Use structured outputs where the provider supports them, JSON mode as a fallback.
  2. Set additionalProperties: false so you get only your fields.
  3. Flatten the schema. Reshape in code afterwards.
  4. Enums for closed sets, always with a catch-all option.
  5. Descriptions on every ambiguous field, including how to handle missing data.
  6. Reasoning fields before conclusion fields.
  7. Make uncertainty expressible β€” nullable types and explicit unknown options.
  8. Bound arrays with maxItems.
  9. Check the finish reason before parsing.
  10. Validate semantics after parsing. Structure is not correctness.

Inspecting or validating JSON?

Format, validate and explore JSON in your browser β€” useful for checking model output and designing schemas, with nothing uploaded.

Open the JSON Formatter β†’

Summary

  • Prompting for JSON fails a few percent of the time, unpredictably.
  • Constrained decoding masks invalid tokens during generation β€” bad syntax is never produced.
  • JSON mode guarantees parsing; structured outputs guarantee shape.
  • Neither guarantees the values are right. The failure mode became silent.
  • Flat schemas, enums and field descriptions materially improve accuracy.
  • Put reasoning fields before answer fields β€” the model cannot revise.
  • Always allow uncertainty or you are formatting a guess.
  • Truncation is the main remaining failure. Check the finish reason.

Frequently Asked Questions

Why does an LLM return JSON wrapped in markdown fences?

Because its training data is full of JSON presented inside code blocks, so that is the most probable continuation when asked for JSON. Prompting against it reduces the frequency but never eliminates it. Constrained decoding removes the possibility entirely, because the fence characters are not valid at that position.

What is the difference between JSON mode and structured outputs?

JSON mode guarantees the output parses as JSON but says nothing about its shape β€” you can get valid JSON with the wrong fields. Structured outputs constrain generation against your schema, so the field names, types and required properties are guaranteed as well.

How does constrained decoding work?

At each generation step the model produces probabilities for every token, and the decoder masks out every token that would make the output invalid against the schema. The model picks from what remains. Invalid syntax is not corrected after the fact β€” it is never generated.

Does a schema guarantee the values are correct?

No. It guarantees structure, not truth. A schema requiring a numeric invoice_total will produce a number, and that number can still be wrong. Constrained decoding solves parsing failures and does nothing for accuracy, which still needs validation and evaluation.

Why does my structured output get truncated mid-object?

The generation hit the max_tokens limit. Constrained decoding guarantees validity of what it produces, not that it will finish β€” if the budget runs out mid-object the result is incomplete and unparseable. Set a limit that comfortably exceeds your largest expected response.

P

Written by Paras

We build free, browser-based file tools and write the reference material we wish existed when we were looking things up. Spotted an error? Tell us and we will fix it.