1:1 mentoring with Big Tech AI engineers
Q7

How do you get reliable structured output? What happens when the JSON doesn't parse?

Fundamentals & Architecture

Structured OutputReliabilityTool UseProduction

Asked at OpenAI · Stripe · Amazon

How to Answer

“First, don’t ask for JSON in prose and hope. Use the provider’s constrained decoding — attach a schema to the request so invalid tokens are masked at sampling time and the output parses by construction.

That buys you syntactic validity, not semantic validity. The model will happily return a perfectly-shaped object with a date in the wrong format, an enum value it invented, or line items that don’t add up to the total it also gave you. So there are two layers: constrained decoding for shape, then my own validator for meaning.

When validation fails, I retry exactly once with the validation error echoed back — ‘currency must be one of USD, EUR, GBP; you sent US Dollars’. That fixes most of them. If the second attempt fails, it goes to a fallback path or a human. It does not loop.”

The deep dive — diagrams, tradeoff tables, and the follow-up trap

Two gates, two different failures

Constrained decoding gets you shape. It does not get you meaning.
Two gates, two different failures Gate 1 — shape schema-constrained decoding: invalid tokens masked at sampling time catches: unparseable JSON, missing required fields, wrong types Gate 2 — meaning your validator, after it parses catches: invented enums, wrong units, totals that don’t reconcile, ids that don’t exist, arrays quietly cut short parses Fail → retry once with the error echoed back → then stop “field currency must be one of USD, EUR, GBP — you sent ‘US Dollars’” · a second failure falls back to a human; it does not loop

What still goes wrong after the JSON parses

FailureWhat it looks likeGuard
Invented enum"status": "in-progress-ish"literal enum in the schema; reject and echo the allowed values
Ambiguous unit"amount": 1250 — cents or dollars?require an explicit currency and minor-unit field
Doesn’t reconcileline items sum to 1180, total says 1250compute the total yourself; never trust the model’s arithmetic
Hallucinated idan order_id that has never existedresolve every id against the system of record before acting on it
Silent truncation3 of 47 line items, and it parsescheck the stop reason, not just whether it parsed

The retry that works

One repair attempt, carrying the specific error. Not a loop, and not a bare retry of the same prompt — that just resamples the same mistake.

def extract(doc, attempt=1):
    resp = client.messages.create(model=MODEL, tools=[INVOICE_SCHEMA],
                                  messages=build(doc))
    if resp.stop_reason == "max_tokens":
        raise Truncated(doc.id)          # parses fine, is missing rows

    obj = resp.content[0].input          # shape already guaranteed
    errors = check_business_rules(obj)   # enums, units, totals, ids
    if not errors:
        return obj
    if attempt == 2:
        return escalate(doc, errors)     # human queue; do not keep looping
    return extract(doc, attempt + 1, repair_hint=render(errors))
REAL SYSTEM

Invoice extraction, ~40K documents a month. Moving from “respond in JSON” to schema-constrained decoding took hard parse failures from ~4.2% to zero. The business-rule validator then caught another ~6.1% that parsed perfectly — mostly invented currency codes and totals that didn’t match the line items. One repair retry cleared ~81% of those, leaving ~1.2% for a human. The truncation check was added after a month of invoices that were quietly missing their last rows.

FOLLOW-UP TRAP

“If decoding is constrained, why validate at all?” — the grammar only guarantees the shape you asked for. Its most dangerous failure is the one that looks like success: hit the token limit mid-array and you can still get a well-formed object with three of forty-seven rows. It parses, it validates against a loose schema, and it is wrong. Check the stop reason on every call.