← All posts
LLM engineeringStructured outputReliabilityAgent architecture

JSON mode isn't a contract: making structured output reliable in production

Constrained decoding gets you syntactically valid JSON, not semantically correct data. Here's the validation, retry, and schema-design layer that actually makes structured output production-grade.

Every model provider now offers some flavor of structured output — OpenAI’s response_format with strict JSON schema, Anthropic’s tool-use forcing, Gemini’s responseSchema. Teams treat “the API guarantees valid JSON” as the end of the problem. It’s the start of a different one.

Constrained decoding (grammar-based token masking, in most implementations) guarantees the output parses. It says nothing about whether the values are right. A model can emit a perfectly well-formed JSON object with a hallucinated order ID, a status field that doesn’t match the actual outcome, or a confidence score it invented because your schema requires one. Syntactic validity and semantic correctness are different failure surfaces, and only one of them is solved for you.

Where it still breaks

The reliability layer that’s actually required

flowchart LR
  P["Prompt + schema"] --> M["Model call
(constrained decoding)"] M --> S["Syntactic check
(parses? matches schema?)"] S -->|fail| RT["Retry with error
fed back into prompt"] RT --> M S -->|pass| V["Semantic checks
(business rules, refs exist)"] V -->|fail| RT V -->|pass| OUT["Accepted output"]

Three layers, not one:

  1. Syntactic validation — you get this mostly for free from strict mode, but don’t skip it. Providers’ “guarantee” still has edge cases (nested oneOf, recursive schemas) where compliance degrades. Validate with a real schema library (ajv, pydantic) on every response regardless of provider promises.
  2. Semantic validation — checks the model provider cannot run because they require your domain data. Does the referenced order ID exist? Is the date in the future when it shouldn’t be? Does the enum value match what your downstream state machine actually expects? This is plain code, and it’s where most real bugs live.
  3. Retry with the error in context — on failure, don’t just resample. Feed the validator’s specific complaint back to the model (“order_id 8823-A does not exist in this account”) so the retry has new information instead of the same odds of repeating the mistake. Cap at 2–3 retries and fall back to a cheaper deterministic extraction or a human queue — don’t loop indefinitely on a model that’s confidently wrong.

Schema design choices that prevent failures upstream

What to log

Log the raw model output, the validation result (syntactic and semantic separately), and which retry attempt succeeded — not just the final accepted value. When a semantic check starts failing at a higher rate after a prompt or model change, that rate is your earliest signal of drift, and you won’t have it without the breakdown by layer. Treat structured-output failure rate as a first-class metric on any agent or extraction pipeline, the same way you’d track latency or cost.

Structured output turns “the model said something” into “the model produced a value your system will act on automatically.” That’s a much higher bar than valid JSON, and it’s on you to enforce it.

Want something like this built for your team?

Get a quote →