How to Get Reliable JSON From LLMs: JSON Mode vs Structured Outputs vs Constrained Decoding
Structured Outputs with grammar-constrained decoding is the most reliable way to get valid JSON from an LLM. Here's how the three mechanisms compare.
Short answer: The most reliable way is grammar-constrained decoding via a strict schema mode — OpenAI Structured Outputs, Gemini's responseSchema, or vLLM/Outlines/llama.cpp on self-hosted models. These mask invalid token logits at each decoding step, so the completed output is guaranteed to match your schema. Plain "JSON mode" only guarantees valid syntax, and prompt-only instructions guarantee nothing.
The three mechanisms
There are exactly three levers, in ascending order of reliability:
- Prompt-only JSON. You ask the model in the system prompt to "respond with valid JSON only, no markdown." Nothing enforces it. The sampler is free to emit any token.
- Provider JSON mode. A flag (
response_format: {type: "json_object"}) that makes the provider validate the output is syntactically valid JSON before returning it. It does not know or check your fields, types, or required keys. - Strict Structured Outputs / grammar-constrained decoding. You supply a JSON Schema (or a formal grammar). At every decoding step the engine builds a token mask that zeroes the probability of any token that would make the partial output un-parseable against the grammar. The output cannot violate the schema because invalid continuations are never sampled.
That last mechanism is the important one. OpenAI Structured Outputs, Gemini, and vLLM all implement it by masking invalid token logits so the completed output matches the schema. This is a decoding-time guarantee, not a post-hoc check.
How they compare
| Prompt-only | JSON mode | Structured Outputs | |
|---|---|---|---|
| Guarantee | none | valid syntax | exact schema |
| Enforces field names/types | no | no | yes |
| Enforces required/enum | no | no | yes |
| Model support | any | most hosted | OpenAI, Gemini, vLLM, Outlines, llama.cpp |
| Latency cost | none | negligible | small (grammar compile + per-step mask) |
| Typical failure mode | fences, prose, JSONL | wrong keys, missing fields, wrapper objects | schema too rigid; blank content in edge cases |
What each actually fixes
The everyday failures fall into a few buckets:
- Markdown fences (
```json). Models wrap JSON in fences even when explicitly told not to. Prompt-only does not fix this. JSON mode and Structured Outputs both do — the raw response is bare JSON. - Prose preamble / "Here is your JSON:". Fixed by JSON mode and above; not by prompting.
- JSONL / multiple objects streamed back to back. Prompt-only fails here constantly. JSON mode fixes syntax framing; Structured Outputs fixes it structurally since the grammar only permits one object.
- Wrapper objects (
{"result": {...}}when you wanted{...}, oritemsrenamed todata). This is the one JSON mode does not fix — the JSON is valid, just not yours. Only Structured Outputs enforces the exact shape. - Blank content. Even constrained modes occasionally return empty content (~3.8% in certain tool-call sequences where the model chooses to call a tool or emits nothing). No decoding constraint saves you from an empty completion — you still need a fallback.
Recommended default per stack
- Hosted API (OpenAI/Gemini). Use Structured Outputs /
responseSchemawithstrict: true. Do not settle for plain JSON mode unless the schema genuinely varies at runtime. - Self-hosted vLLM. Use the
guided_json/ structured-outputs parameter (backed by xgrammar/Outlines). Same decoding-time guarantee as the hosted APIs, at essentially zero extra infra. - On-device (llama.cpp / GGUF). Use GBNF grammars or a JSON-schema-to-grammar conversion. This is where constrained decoding matters most, because small local models are the worst at following prompt-only JSON instructions.
A defensive fallback parser
Even with a good mechanism, wrap parsing defensively for the blank-content and fallback-provider cases:
import json, re
def parse_llm_json(text: str):
if not text or not text.strip():
raise ValueError("empty completion") # ~3.8% blank case
# strip ```json ... ``` fences if present
m = re.search(r"```(?:json)?\s*(.*?)```", text, re.S)
candidate = m.group(1) if m else text
# trim to the outermost object/array
start = min([i for i in (candidate.find("{"), candidate.find("[")) if i != -1])
end = max(candidate.rfind("}"), candidate.rfind("]"))
return json.loads(candidate[start:end + 1])Use this only when the schema-constrained mode is unavailable (older model, third-party gateway, tiny local model). If you control the model, constrain the decoder instead of cleaning up after it.
Reach for schema-constrained decoding first; keep a defensive parser only for the cases where you can't.
Related: more writing.
Frequently asked
- Is JSON mode the same as Structured Outputs?
- No. JSON mode only guarantees syntactically valid JSON; Structured Outputs enforces your exact schema — field names, types, required keys, enums — via grammar-constrained decoding.
- Why does the model still add ```json fences?
- Prompt-only instructions are unreliable; models add fences even when told not to. Use a schema-constrained mode (which returns bare JSON), or strip fences defensively before parsing.
- Can I get schema-constrained JSON on self-hosted models?
- Yes. vLLM (guided_json), Outlines, and llama.cpp (GBNF grammars) all support grammar/JSON-schema constrained decoding with the same decoding-time guarantee as hosted APIs.
- Does constrained decoding guarantee I always get output?
- No. It guarantees any output matches the schema, but models still occasionally return blank content (~3.8% in some tool-call sequences). Always keep a fallback that handles empty completions.
Keep reading
Why I don't use LangChain in production RAG
Frameworks are great for a demo. In production, the abstraction you can't see into costs more than it saves.
ReadWhen NOT to use RAG (RAG vs fine-tuning vs prompting)
RAG became the default reflex for every LLM problem. Often it's the wrong tool. A straight decision framework for RAG vs fine-tuning vs just prompting.
ReadPrompt Caching Explained: How to Cut LLM Cost by up to 90% Without Losing Quality
Prompt caching reuses precomputed KV state for identical prompt prefixes, cutting cost up to 90% and latency up to 85% with no quality change.
ReadGet new essays by email
Occasional field notes on production RAG and LLM systems. No spam, unsubscribe anytime.