LLM applications and RAG

Structured Output: Getting JSON You Can Trust

Constrained decoding guarantees your JSON parses. It guarantees nothing about the values inside it. The schema design, the abstention channel, the retry discipline and the measurements that separate a working extractor from a confident one.

On this page
  1. Valid JSON and correct JSON are different problems
  2. The four ways to ask for JSON, and what each one guarantees
  3. Schema design is where the reliability actually lives
  4. Give the model a legal way to say nothing
  5. The four-layer output contract
  6. Validate, then repair in the right order
  7. One big schema or several small ones
  8. What it costs, and where the retries hide
  9. Testing an extractor without fooling yourself
  10. The schema and prompt, ready to lift

Valid JSON and correct JSON are different problems

The short answer

Schema-constrained decoding solves one problem completely: the output will parse and it will match the shape you declared. It solves nothing about whether the values are right, whether they came from the source document, or whether the model should have answered at all. Worse, forcing a shape can cause fabrication, because a model with no legal way to express uncertainty will emit a well-formed guess. The reliability work is in schema design, an explicit abstention channel, and field-level measurement, not in the decoding flag.

The progression is familiar. A prompt asking for JSON returns the occasional trailing comma, markdown fence or helpful preamble. You switch on a schema and those failures disappear overnight. Six weeks later somebody in finance notices that a supplier name has been quietly filled in on documents where it never appeared, and it has been happening since the day the shape errors stopped.

That is not a coincidence. Removing the shape errors removed your only visible symptom. A malformed response is loud and lands in an error queue. A well-formed wrong response looks exactly like a well-formed right one, flows into your database, and is found weeks later or never.

  • Shape is not truthA grammar constrains which tokens are legal. It has no view on whether the value is in the document.
  • No exit means inventionIf your schema has no legal way to say not found, the model will produce something that fits instead.
  • Field order is causalGeneration runs left to right, so a field emitted earlier conditions every field after it. Put evidence before the value.
  • Parse rate measures nothingOnce you switch on constrained decoding, parse rate is pinned at one hundred percent and stops carrying information.
  • Truncation is not a retry caseIf the output hit the token ceiling mid-object, retrying identically truncates again. Cut the scope instead.
Terms used precisely
Structured output
A generation mode where the model's output is required to conform to a declared schema, so the response parses into a known shape without any post-processing or repair step.
Constrained decoding
The mechanism behind it: at each generation step the sampler is restricted to tokens that can still lead to a valid document under the schema, which makes an invalid response impossible rather than unlikely.
Abstention channel
An explicit, schema-legal way for the model to report that it has no answer, usually a nullable value paired with a required reason code, so that saying nothing is a valid response rather than a rule violation.
Grounding
The check that an extracted value actually appears in the source material, most cheaply done by requiring the model to return the exact span it copied and verifying that span exists in the input.
Fabrication rate
The share of cases where a field is genuinely absent from the source and the system returns a value anyway. It is the single most useful number for a structured extractor and almost nobody measures it.

The four ways to ask for JSON, and what each one guarantees

These are not four flavours of the same thing. They guarantee different properties and fail in different places, and mixing them up produces the wrong debugging instinct.

MethodGuaranteesDoes not guaranteeUse it when
Ask in the promptNothing. It is a requestParsing, field presence, enum membershipPrototyping only, or a provider with no schema support
JSON modeThe output parses as JSONYour fields, your types, your enumsYou control the shape downstream and want a cheap floor
Tool or function schemaShape conformance, plus a natural place for a refusal pathValue correctness, or that the tool was the right choiceThe model must also choose between several actions
Schema-constrained decodingShape, types, required fields, enum membershipThat any value is true, present in the source, or safe to act onExtraction and classification, which is most production work
Generate then repairNothing on its ownAnything. It hides systematic errors under a coercion layerNever as a primary strategy. Acceptable as a logged fallback
Methods for structured output, by what they actually guarantee
Silent repair is the expensive habit

A repair layer that trims fences, fixes quotes, coerces a string into a number and moves on keeps your error rate at zero and your data quietly wrong. If you keep a repair path, make it loud: log every repair with the raw output, and treat a rising repair rate as a regression rather than as the system working.

Schema design is where the reliability actually lives

The schema is not a type declaration. It is part of the prompt, it is read on every call, and its structure changes what the model produces. Six design choices carry most of the outcome.

Order fields so that evidence comes before conclusions

Generation is autoregressive. Every token is conditioned on the tokens already emitted, which means a field's position in your schema is a causal choice and not a cosmetic one. Put the quoted source span before the extracted value and the model has to find text before it can commit to an answer. Put it after and you get a value first, then a span invented to justify it. The same logic applies to a reasoning field: before the answer it is reasoning, after the answer it is a press release.

Prefer closed sets, and split large ones

An enum is the only part of a schema that constrains meaning rather than type, so use it wherever the legal answers are known. The cost is tokens, since the list is sent on every call, and a taxonomy with a hundred members is both expensive and harder to discriminate within. Above roughly twenty options, a coarse call followed by a narrow one usually beats a single long list on both accuracy and cost.

Make fields required and nullable, not optional

An optional field lets the model skip it, and you cannot distinguish deliberate omission from a field it forgot. A required non-nullable field forces a value even when there is none, which is where fabrication comes from. Required and nullable is the combination that works: the model must address every field, and null is an available answer. Pair it with a required reason code and null becomes a data point rather than a hole.

Keep it shallow and avoid top-level unions

Every level of nesting is structure the model holds open while it generates, and deep objects are where truncation does the most damage, since a cut at depth four destroys the whole document. A top-level union is worse, because the model commits to a branch early and spends the rest of the generation defending that commitment. For genuinely different document types, make the type a first field with an enum and let a second call handle the branch.

Cap every array and let the model report the cap

An unbounded array is an invitation to generate until the token limit, and truncation inside an array is the most common cause of an unparseable response even with constrained decoding, because the ceiling is enforced by the sampler and not by the grammar. Set a maximum, and add a flag the model can set to say the list was longer than the cap. Silence about truncation is worse than the truncation.

Take numbers and dates out of the model's hands

Models format numbers the way the surrounding text does, so you will receive thousands separators, currency symbols and decimal commas. Declare amounts as strings with a strict pattern and parse them in your own code where a failure is visible. Dates are the same problem with a sharper edge: a document showing 03/04/2026 is genuinely ambiguous, and a model that resolves it is guessing. Require ISO format and give it a way to report the ambiguity instead.

Field descriptions are the last lever. They act as instructions and are billed on every call, so a hundred-field schema with a paragraph on each becomes a permanent tax. Write them short and imperative, and keep the long explanation in the system prompt where it appears once.

This is the highest-value change on most extraction systems, and it takes an afternoon. A model constrained to produce a string will produce a string. If the document has no purchase order number, and your schema says purchase_order is a required string, the only legal moves available to the sampler are strings that look like purchase order numbers. The system is behaving exactly as designed, which is the problem.

The fix has three parts. Make the value nullable, add a required reason code with a small enum so that null carries information rather than mere absence, and say plainly in the system prompt that a blank field is a correct answer. The model's default posture is helpfulness, and helpfulness looks like filling things in.

  • absent is the ordinary case: the field is not in this document and that is normal.
  • illegible separates a scanning problem from a content problem, and routes to a different queue.
  • ambiguous_multiple_candidates is the one that saves you. Two plausible supplier names is a human decision, not a coin flip.
  • wrong_document_kind catches the delivery note that arrived in the invoice pipeline before it corrupts twelve other fields.

The reason codes then become operational signal. A rising illegible rate is an upstream scanner problem, not a prompt problem. A rising ambiguity rate on one supplier means their template changed. Neither is visible in a system whose only vocabulary is a value or a crash.

Reward abstention in the prompt, explicitly

Models are trained toward helpfulness, and an empty field reads as unhelpful. State the trade explicitly: a blank field is visible to a human reviewer and costs a minute, a fabricated field is invisible and costs a corrected ledger entry. Saying this in the system prompt measurably changes behaviour on ambiguous documents, which is why the prompt below ends on that line.

The four-layer output contract

Teams talk about structured output as one property, which is why they stop work after the first layer is solved. It is four properties, each enforced in a different place, each with its own failure and its own test. Only the first is free.

Framework

The Four-Layer Output Contract

Four separate promises hide inside the phrase trusted JSON. Constrained decoding delivers the first one and none of the others, and each layer below it needs its own enforcement point and its own number.

01
Shape: it parses and matches the schema

Enforced by the decoder. Types are right, required fields present, enums respected. Once constrained decoding is on, this layer is solved permanently and its metric becomes uninformative. Stop reporting it as a health measure, because a flat line at one hundred percent tells you nothing about the day the extraction quality collapsed.

02
Vocabulary: the values are inside the allowed world

Enforced by your own validator after parsing, not by the schema. Dates are real dates, an amount parses to a number, an identifier matches its check digit, a supplier code exists in your supplier table. Schemas express types, not domains. This layer catches the fluent nonsense that is syntactically perfect and operationally impossible.

03
Referent: the value traces back to the source

Enforced by requiring an evidence span and checking that the span appears in the input, usually with normalised whitespace. This is the layer that catches fabrication, and it is the one almost nobody builds. A value whose evidence span cannot be located in the document is not an extraction, it is a suggestion, and it should be routed to a human rather than written to a database.

04
Consequence: the value is safe to act on

Enforced by business rules and human gates, downstream of everything else. An amount above an approval threshold, a supplier flagged for review, a date in the future, a total that disagrees with the line items. This is the layer where the cost of being wrong is priced, and it is the only place where a correct extraction can still be the wrong thing to act on.

05
The rule that follows: one metric per layer

Report shape conformance, validator pass rate, evidence match rate and downstream rejection rate as four separate numbers. A single accuracy figure averages them into something that cannot be acted on, and it will keep looking healthy while one layer quietly rots.

The layers are also a build order. Do them in sequence and each one narrows what the next has to handle. Skip to the fourth and you will find yourself writing business rules that exist only to catch invented values, which is an expensive way to solve a problem that a nullable field would have prevented.

Validate, then repair in the right order

The failure path matters more than the happy path, because the happy path is now guaranteed. Run the stages in this order and log the stage that stopped each call, since the distribution across stages is the most useful debugging artifact you will have.

  1. Check the finish reason before you check the outputfree

    If generation stopped because it hit the token ceiling, the output is truncated and the content is irrelevant. This is not a retry case. Retrying the same request truncates again, at the same place, for the same money. Reduce the field set, raise the output reservation, or split the document.

  2. Parse and validate against the schemamicroseconds

    With constrained decoding this should never fail. If it does, you have a provider incident, a schema silently rejected and fallen back from, or a length cut you missed at step one. Alert rather than repair, because the interesting information is that it happened at all.

  3. Run the domain validatormilliseconds

    Dates parse, amounts parse, identifiers resolve against your own tables, enums map to live internal codes. This is your code and it should be strict. Every rule here started as a real bad row, so keep a comment naming it.

  4. Verify the evidence spansmilliseconds

    For every non-null tracked field, confirm the returned span exists in the input after whitespace normalisation. Mismatches are fabrication, and they go to a human queue rather than into a retry loop, because a retry usually produces a different fabrication rather than an admission.

  5. Retry once, with the error attachedone extra call

    Send the failed output back with the specific validation error and an instruction to fix only that field. One retry catches most transient failures. A second rarely helps and a third never does, so cap it at two and make the cap a constant somebody can find.

  6. Dead-letter with everything attachedhuman time

    Store the input, the prompt version, the schema version, the raw output and the stage that failed. A dead letter without the prompt version is close to useless three weeks later. Automation error handling covers the queue design and who reads it.

Retries and side effects

If your extraction call sits inside a workflow that writes to a ledger, sends an email or creates a ticket, a retry must not repeat the write. Give every document an idempotency key derived from its content hash and check it before the side effect, not after. The details are in idempotency in automation, and this is the bug that turns a quiet retry into a duplicate payment.

One big schema or several small ones

Once a schema passes about twenty fields, splitting the extraction into field groups usually wins, and the reason is not accuracy in the abstract. It is that failure stops being all-or-nothing.

One call, whole schemaSplit into field groups
Cost of the document tokensPaid oncePaid once per group, unless caching covers it
Schema token overheadOne large blockSeveral small blocks, often less in total per group
Effect of one bad fieldThe whole document retriesOne group retries, the rest is already banked
Truncation riskRises with field countLow, each output is small
Cross-field consistencyThe model sees everything at onceNeeds a reconciliation step in your code
LatencyOne round tripLower per call, and the groups run in parallel
Attribution when quality dropsHard, one number for forty fieldsStraightforward, the group tells you where

The deciding question is whether the fields are genuinely interdependent. Header fields and totals belong together because they constrain each other. Line items belong in their own call because they are repetitive and long. Anything requiring judgement, a category or a routing decision, belongs in a third call that can be evaluated and versioned on its own. If prefix caching applies to your stable document prefix, the cost argument for a single call gets weaker, which is worth checking before you assume splitting is expensive.

What it costs, and where the retries hide

Two line items get left out of structured extraction budgets. The schema is re-sent on every call, so a large schema is a fixed tax multiplied by volume. And the retry rate multiplies everything, including the document tokens you already paid for once. Both are easy to compute and neither shows up until the invoice does.

Monthly cost of a structured extractor, retries included

Prices are starting defaults in your own currency per million tokens, not quotes. Replace them with your last billing export. Count schema tokens with the provider's tokenizer rather than estimating, since JSON punctuation and long field names tokenize worse than prose.

0Total per month
0Of which schema overhead
0Of which retries
0Cost per 1,000 documents

Run it with your real schema size and the answer is often uncomfortable. Full-sentence field descriptions, a long enum and a system prompt nobody has trimmed since launch can push the fixed overhead close to the document itself on short inputs. Token cost arithmetic has the general version of this calculation.

Testing an extractor without fooling yourself

The metric that matters is per field, and the case that matters most is the one where the answer is nothing. An extractor that scores well on documents containing every field, and fabricates on documents that do not, will look excellent on any test set drawn from your happy path.

Build the set from real documents and include the awkward population: fields that are genuinely absent, partly illegible scans, a document of the wrong type that slipped into the pipeline, two invoices in one file, and one where the field label appears but the value does not. That last case is the sharpest test there is, because the label alone is enough to pull a plausible value out of a model with no way to abstain.

MeasurementComputed asCatches
Field precisionCorrect values divided by values returned, per fieldFabrication and misreading, one field at a time
Field recallCorrect values divided by values genuinely presentOver-abstention, a model that has become timid
Fabrication rateValues returned where the field is absent, over documents where it is absentThe failure that matters most and is almost never measured
Evidence match rateSpans found in the input, over non-null valuesGrounding drift, usually the first sign of a model change
Reason code distributionCounts per abstention code over timeUpstream problems: scanner quality, template changes, misrouting
Truncation rateCalls stopped by the length ceilingSchemas that have quietly outgrown the output reservation
What to measure, and the failure each measurement catches

Freeze the set, version it, and run it on every prompt change and every model change. Providers update models behind stable names, and an extractor steady for months can shift in a week with nothing in your repository changing. Writing evals for LLM systems covers the mechanics, and prompts as code covers pinning prompt and schema together.

Before a structured extractor goes to production
0 of 9 done

The schema and prompt, ready to lift

The pair below is a working starting point. The schema carries the abstention channel, evidence-before-value ordering, string amounts with a pattern, a capped notes array, and a document-kind field decided first. The system prompt states the trade the schema alone cannot express.

Extraction schema with an abstention channeljson
{
  "name": "invoice_extraction",
  "strict": true,
  "schema": {
    "type": "object",
    "additionalProperties": false,
    "required": ["document_kind", "supplier_name", "invoice_number", "invoice_date",
                 "total_amount", "currency", "line_item_count", "notes"],
    "properties": {

      "document_kind": {
        "type": "string",
        "enum": ["invoice", "credit_note", "statement", "delivery_note", "unreadable", "other"],
        "description": "Decide this FIRST. Everything below is conditioned on it."
      },

      "supplier_name": {
        "type": "object",
        "additionalProperties": false,
        "required": ["evidence", "value", "not_found_reason"],
        "properties": {
          "evidence": {
            "type": ["string", "null"],
            "maxLength": 200,
            "description": "The exact span copied from the document. Emitted BEFORE the value."
          },
          "value": {
            "type": ["string", "null"],
            "description": "Null unless the evidence field above contains it verbatim."
          },
          "not_found_reason": {
            "type": ["string", "null"],
            "enum": ["absent", "illegible", "ambiguous_multiple_candidates", "wrong_document_kind", null],
            "description": "Required when value is null. This is the legal way to say nothing."
          }
        }
      },

      "invoice_number": { "$ref": "#/$defs/tracked_string" },
      "purchase_order":  { "$ref": "#/$defs/tracked_string" },

      "invoice_date": {
        "type": ["string", "null"],
        "description": "ISO 8601 only. For an ambiguous form such as 03/04/2026, return null and set date_ambiguity."
      },
      "date_ambiguity": {
        "type": ["string", "null"],
        "enum": ["day_month_order_unclear", "year_missing", "multiple_dates_present", null]
      },

      "total_amount": {
        "type": ["string", "null"],
        "pattern": "^-?[0-9]+(\\.[0-9]{1,2})?$",
        "description": "Digits and a decimal point only. No separators, no symbol. String, not number: your code parses it."
      },
      "currency": {
        "type": ["string", "null"],
        "enum": ["GBP", "EUR", "USD", "INR", "AED", null]
      },

      "line_item_count": { "type": ["integer", "null"], "minimum": 0, "maximum": 500 },

      "notes": {
        "type": "array",
        "maxItems": 5,
        "items": {
          "type": "string",
          "enum": ["handwritten_annotation_present", "page_missing", "poor_scan_quality",
                   "multiple_invoices_in_one_file", "totals_do_not_add_up"]
        }
      }
    },
    "$defs": {
      "tracked_string": { "$comment": "same three-property shape as supplier_name above" }
    }
  }
}
System prompt for the same extractortext
You extract fields from supplier documents. You do not interpret them, summarise them,
or correct them.

RULES, in priority order:

(1) Evidence before value. For every tracked field, write the `evidence` span first, copied
   character for character from the document. Then write `value`. If you cannot copy a span
   that contains the value, `value` is null.

(2) Null is a correct answer. A missing field is normal. When you set a value to null, set
   `not_found_reason` to the code that describes why. Never infer a value from another field
   or from what an invoice usually contains.

(3) Never compute. Do not add line items, convert currencies, reconcile totals, or normalise a
   date you had to guess at. If the printed total disagrees with the line items, report the
   printed total and add "totals_do_not_add_up" to `notes`.

(4) Ambiguity is reported, not resolved. Two candidate supplier names means
   `ambiguous_multiple_candidates`, not the one that looks more likely. A date of 03/04/2026
   with no other signal means null plus `day_month_order_unclear`.

(5) One document per call. If the file contains more than one invoice, extract the first and add
   "multiple_invoices_in_one_file" to `notes`.

You are evaluated on how often you correctly return null, not only on how often you return a
value. A blank field is visible to a human. A fabricated one is not.

Adapt the field names and enums, keep the four structural choices, and add each new reason code only when a real document forces it. Codes invented in advance go unused and cost tokens forever. This pattern sits underneath most of our custom LLM applications work, and the mechanics of reading messy files are in extracting data from documents.

Cite this

Constrained decoding guarantees that structured output parses, not that it is true. Without a schema-legal way to say not found, a required field turns an absent value into a fabricated one.

Questions readers ask next

Does constrained decoding stop hallucination?
No, and it can increase one kind of it. Constrained decoding restricts the sampler to tokens that keep the output valid under your schema, which eliminates malformed responses entirely and says nothing about whether a value is true or present in the source. If a required field cannot legally be null, the only available outputs are values, so the model supplies one. Add a nullable value with a required reason code and the pressure has somewhere else to go.
Should I use tool calling or a response schema for extraction?
For extraction, a response schema is the more direct tool, because there is one thing you want back and no decision about which action to take. Tool calling earns its place when the model must also choose between several operations, or when you want a natural path for it to call nothing at all. The guarantees overlap heavily, so choose on the shape of the decision rather than on a belief that one is more reliable than the other.
How many retries should a structured output call get?
Cap it at two, and make the first retry carry the specific validation error with an instruction to fix only the offending field. Beyond that the returns collapse, because a model that failed twice on the same input is usually facing an ambiguous document rather than a transient problem. Route it to a human queue with the input, the prompt version and the raw output attached. Check the finish reason first, since a truncated response needs a smaller request rather than another attempt.
Why does my model return numbers with commas and currency symbols?
Because it is copying the formatting of the surrounding text, which is what extraction from a document means. Declaring the field as a JSON number does not reliably fix it, it converts the problem into a type error. Declare amounts as strings with a strict pattern, state the format in the field description, and parse in your own code where a malformed value raises an error you can see and count.
Is a large schema better than several small ones?
Past roughly twenty fields, splitting into field groups usually wins, mostly because failure stops being all-or-nothing. One group can retry while the rest is already banked, truncation risk drops, and a quality regression tells you which group it lives in. The cost of re-sending the document to each group is the counterweight, so check whether prefix caching covers your stable prefix before assuming a split is more expensive.
How do I test whether the model is inventing values?
Measure fabrication rate directly. Assemble documents where a given field is genuinely absent, run the extractor, and count how often it returns a value anyway. Include the hardest version, a document where the field label appears but the value does not, since the label alone is often enough to produce a plausible answer. Then require evidence spans and check that each returned span exists in the input, which converts fabrication from a statistical worry into a per-call test.
What should I log for every structured output call?
The input reference, the prompt version, the schema version, the model identifier as the provider returned it, the finish reason, the raw output before parsing, the validation stage that failed if one did, and the per-field evidence match result. The two that get skipped and are always wanted later are the raw output and the schema version, because without them you cannot tell whether a past result came from a different contract than the one running today.
Cite this

ChatGPTalker. "Structured Output: Getting JSON You Can Trust." chatgptalker.com, 2026-08-26. https://chatgptalker.com/guides/structured-output-from-llms/

Rather have it built than read about it?

Send the process you want automated. You get a scoped plan back, with the build shape, the stack and a realistic timeline.

Start a project