LLM Structured Output in Practice: JSON Mode, Function Calling and Constrained Decoding (2026 Edition)
An LLM that "misbehaves" — JSON with an extra line of chatter, a misspelled field name, a hallucinated tool argument — is the wall most AI projects hit before production. This article gives a shipping-ready structured-output toolkit: a precision-vs-compatibility comparison of the three constraint mechanisms (JSON Mode / Function Calling / constrained decoding), six iron rules for schema design (flatten it, prefer enums, drive with examples), a four-step parse-and-validate defense chain (validator + feedback retry + degradation path), hallucination-proofing for agent tool calls (schema constraints + parameter whitelists + call audit), the high-frequency pitfalls of multilingual output and nested structures, and a pre-launch structured-output self-audit checklist.
The Three Walls LLMs Hit Before Production Are All Structured-Output Problems
AI demos dazzle; production ships break. Ten out of ten times it is the same place: the model output “misbehaves”. A support-ticket system asks the model to emit JSON for the database, and 3% of responses carry an extra line of “Sure, here is the ticket info:”. An agent calls a tool with a fabricated order_id, the tool errors, the workflow stalls. A content pipeline asks for category labels and gets “digital”, “digital products”, “3C digital” in the same day — downstream reports are full of dirty data.
Structured output is not “please output JSON” in the prompt. It is an engineering problem: constraint mechanism selection → schema design → parse and validate → failure fallback → monitoring. This article gives the complete toolkit we have validated in production.
1. Three Constraint Mechanisms: Precision vs. Compatibility
1.1 Comparison
| Mechanism | Guarantees | Does NOT guarantee | Compatibility | Use case |
|---|---|---|---|---|
| Pure prompt (“please output JSON”) | Nothing | Validity, schema — nothing | Everything | Deprecated, fallback only |
JSON Mode (response_format) | Output is valid JSON | Field names / types / enums | Most cloud APIs | Fallback for older models |
| Structured Outputs / Function Calling | Output 100% matches the JSON Schema | Factual correctness of arguments | OpenAI / Anthropic / Qwen / DeepSeek and other major APIs | Default for 99% of cases |
| Constrained decoding (guided decoding) | Per-token grammar enforcement | Same as above | Private vLLM / Ollama / llama.cpp | Local deployments, offline environments |
Three selection conclusions:
- Use structured outputs on every cloud API (OpenAI calls it structured outputs, Anthropic tool use, Qwen/DeepSeek a json_schema variant of response_format) — this is the 2026 baseline capability. Use it.
- Private deployments (vLLM etc.) use constrained decoding — vLLM natively supports
guided_json/guided_choice(outlines-based grammar constraints); Ollama supportsformat: json(valid JSON only). In private environments the model has no “structured output switch” — the constraint must move down to the decoding layer. - Small models (under 7B) get degraded handling — small models follow long schemas poorly. Keep the schema minimal (iron rules below) and always pair it with parse-retry fallback.
1.2 Key Insight: You Constrain Shape, Not Facts
A schema guarantees field names, types and enum membership — not that the value exists in your business. The model can output a perfectly formatted, semantically fabricated order_id: "ORD-2026-9981". This is not a flaw in the constraint mechanism; it is the nature of LLMs — they predict the next token, they do not query your database.
So the full definition of structured output is: constraint (the decoding layer guarantees shape) + validation (the execution layer guarantees semantics) — you need both.
2. Six Iron Rules for Schema Design
2.1 The Six Rules
| # | Rule | Anti-pattern | Do this |
|---|---|---|---|
| 1 | Nesting ≤2 levels, ≤8 fields per level | order.items[].attributes[].value | Flat array + foreign key (item_id referencing the attributes table) |
| 2 | Enum over open fields | status: "已完成" (free text) | status: ["pending","paid","refunded"] (enum) |
| 3 | Self-explanatory names + description | code (what code?) | refund_reason + description explaining the values |
| 4 | 1–2 complete examples in the prompt | Schema definition only | Schema + few-shot complete output examples |
| 5 | Minimize required | All 12 fields required | 3 core fields required, rest optional with defaults |
| 6 | No 2-D arrays | tags: [["A","B"],["C"]] | tags: ["A","B","C"]; grouping via foreign key |
2.2 A Production-Grade Schema Example
Support-ticket classification (simplified from a real delivery):
{
"type": "object",
"properties": {
"category": {
"type": "string",
"enum": ["billing", "technical", "account", "other"],
"description": "Primary ticket category, by the user’s underlying request"
},
"urgency": {
"type": "string",
"enum": ["low", "medium", "high"],
"description": "high = affects money or a core function; medium = degraded with a workaround; low = inquiry"
},
"summary": { "type": "string", "maxLength": 100, "description": "One-sentence summary" },
"action": {
"type": "string",
"enum": ["auto_reply", "human_agent", "create_ticket"],
"description": "Recommended handling path"
}
},
"required": ["category", "urgency", "action"],
"additionalProperties": false
}
Three details matter: additionalProperties: false (no free-field invention — only truly enforced under structured outputs), the urgency description carries executable decision criteria (not filler like “urgency level”), and summary is the only open-text field, with a length cap.
3. Parse and Validate: the Four-Step Defense Chain
3.1 The Chain
model output → ① pydantic validation → ② on failure: feed the error back and retry (≤2) → ③ still failing: business degradation
↓ pass
④ business execution (re-validate argument existence before tool calls)
① Validate fully: json.loads alone is not validation. Compile the schema into a validator (pydantic for Python, zod for TS) — missing fields, wrong types and out-of-enum values are all caught.
② Retry with feedback: a blind retry (same prompt again) only succeeds ~60% of the time. Feed back the validation error — “your last output was missing the required field urgency; please re-emit” — and retry success climbs above 95%. Cap retries at 2; a third attempt almost certainly fails, so degrade instead.
③ Pre-set degradation: before each scenario goes live, decide what “still failing” means:
| Scenario | Degradation path |
|---|---|
| Ticket classification | Default other + human_agent (route to human — slow rather than wrong) |
| Data extraction | Skip the field, null it, queue for manual backfill |
| Agent tool argument | Fail the step, roll back one step and re-plan |
| Content generation | Fall back to a human template |
④ Prevent truncation: set max_tokens to “schema maximum possible length × 1.5”. JSON truncated at max_tokens is unrecoverable (missing tokens cannot be patched) — only a full retry works. Budget it; do not starve max_tokens to save tokens.
3.2 Monitoring
Parse-failure rate on the dashboard, three rules:
- Rate >5% (baseline is typically <1%) — a regression signal after any prompt/model/schema change.
- Rate spikes 3× — top cause: a silent model version upgrade (vendor-side canary); second: a prompt change.
- Single scenario >10% — that scenario’s schema or few-shot examples need rework.
4. Agent Tool Calls: Three Layers Against Hallucinated Arguments
4.1 The Three Layers
| Layer | Mechanism | Blocks |
|---|---|---|
| 1. Schema | Tool arguments defined as JSON Schema + descriptions | Wrong types, missing args, out-of-enum values |
| 2. Validation | Verify argument existence in the data layer before execution | Hallucinated arguments (right format, nonexistent) |
| 3. Granularity | Split big tools into small ones, ≤4 arguments per tool | Argument “padding” behavior (more args = more fabrication) |
Layer 2 is the key one. Example (order lookup tool in a ticket system):
def get_order(order_id: str) -> dict:
order = db.orders.find(order_id)
if not order:
# Do not raise — feed "does not exist" back to the model so it can retry with a different argument
return {
"error": f"order_id '{order_id}' does not exist",
"hint": "Extract the order number the user provided in the conversation, or call search_orders",
"suggestions": db.orders.recent(user_id, limit=3) # offer candidates
}
return order.data
Tool-error design principle: the error message is written for the model — it must contain “why it failed + what to do next + candidate values”, not a human-facing stack trace.
4.2 Tool Descriptions Matter More Than the Schema
The model decides “whether to call it, and with what” by reading the tool description. Two lines that work:
Use when the user asks about order status or requests a refund (do NOT use for inquiries).
Argument order_id: the order number extracted from the user’s message, format ORD-YYYY-NNNN;
if the user has not provided one, call search_orders first — never guess.
“When to call” (trigger condition) + “where the argument comes from” (argument source) + “what if missing” (fallback action) — with all three, misfire rates drop by an order of magnitude.
5. High-Frequency Pitfalls: Multilingual and Long-Text Output
5.1 Mixed Languages
Let the JSON “structure” speak English and the “content” speak the user’s language:
- Field names and enum values: fixed English (
status: "paid"); downstream code matches on English. - Content fields (summary, reply, …): the schema description says “this field is in Chinese”.
- Anti-pattern: unconstrained, the model emits
status: "已完成", the database stores two representations, and reporting is dirty.
5.2 Long-Structure Generation: Two-Phase Calls
Cramming a full report into one output field = guaranteed truncation + retry costs. The correct design is skeleton-then-fill:
Call 1: emit the skeleton
{ "sections": [ {"id": 1, "title": "...", "outline": "key points for this section"}, ... ] }
Calls 2..N: fill each section (parallelizable)
input = document requirements + skeleton + this section’s outline
output = this section’s Markdown
Assembly: the program concatenates (section id as anchor)
Bonus: a failed section re-runs in isolation; sections fill in parallel, so total latency is often shorter; and the skeleton is reviewable — a human can restructure sections before filling starts, instead of waiting for the whole document to realize the direction was wrong.
6. Pre-Launch Self-Audit Checklist
| Check | Pass criteria |
|---|---|
| Constraint mechanism | structured outputs on cloud APIs / guided decoding on private; not reliant on prompt alone |
| Schema flatness | nesting ≤2 levels, ≤8 fields per level, no 2-D arrays |
| Enum coverage | every closed-value field is an enum; descriptions carry executable criteria |
| additionalProperties | set to false (only truly enforced under structured outputs) |
| Few-shot examples | 1–2 complete output examples in the prompt, fully consistent with the schema |
| max_tokens budget | schema max length × 1.5, with truncation-rate monitoring |
| Validator | pydantic/zod full-schema validation, not json.loads |
| Retry mechanism | failure feedback retry ≤2 times, retry success rate instrumented |
| Degradation path | pre-set per scenario (human / skip / re-plan), no unbounded retries |
| Tool argument validation | existence check in the data layer before execution; error message carries “why + next step + candidates” |
| Tool granularity | ≤4 arguments per tool; description covers trigger + argument source + fallback |
| Monitoring | parse-failure dashboard + 3 rules (>5% / 3× spike / single scenario >10%) |
Structured Output Is the “Data Contract” of an LLM Application
Latency, cost and quality are the three engineering metrics of an LLM application — but structured output is the precondition for all three. If output cannot be parsed, cost cannot be computed (you do not know whether the call succeeded or failed), quality cannot be scored (no automatic grading), and agents cannot run (every tool waits for valid arguments).
In the AI customer-service, data-extraction and agent-workflow projects we have delivered, structured-output parse success stays above 99%. That is not because we “picked a better-behaved model” — it is this defense chain: constraint selection (structured outputs / guided decoding) → six schema iron rules → four-step parse defense → three-layer tool hallucination-proofing. If your AI project is stuck on “the model will not listen”, bring us your schema and your failure samples — we will first do a structured-output health check (failure attribution + defense-chain gap analysis), then talk implementation.
Further reading:
- LLM Cost Management in Practice — the cost arithmetic of parse retries and degradation paths
- AI Agent Orchestration in Practice — where tool calls sit in agent orchestration
- LLM Model Selection and Routing — structured-output capability differences across models
- AI Customer Service in Practice — a complete production case of the ticket-classification schema
- LLM Evaluation in Practice — automated quality evaluation after structured output
Need a structured-output design, an agent toolchain, or parse-success-rate optimization? Contact us for a free assessment.
FAQ
What is the difference between JSON Mode, Function Calling, and constrained decoding? How do I choose?
Constraint strength increases, compatibility decreases: ① JSON Mode (response_format: json_object) — the model guarantees the output is valid JSON, but not that it matches your schema: field names can be misspelled, extra or missing fields still happen. ② Function Calling / structured outputs (OpenAI structured_outputs, Anthropic tool use) — output is constrained by a JSON Schema; field names, types and enum values are enforced. Every major API supports it; it is the default for 99% of cases. ③ Constrained decoding (outlines / guidance / lm-format-enforcer, grammar enforced at the inference engine level) — masks illegal tokens one by one, guaranteeing 100% conformance. Use it for private deployments on local engines (vLLM/Ollama) that are OpenAI-compatible but lack structured outputs. Decision path: cloud API → structured outputs directly; private vLLM → constrained decoding (vLLM has native guided decoding); old or small models (under 7B) → prompt + JSON Mode + parse-retry fallback.
I already use Function Calling — why does the model still invent arguments?
The schema constrains shape, not facts. The model will happily fill in a value that is type-valid but semantically fabricated (an order_id that matches the format but does not exist in your database). A schema cannot block hallucination, only format errors. Defend in two layers: ① parameter whitelist validation — before the tool executes, verify the argument against your data layer (the order_id must exist); if invalid, reject it and feed the model back "this parameter does not exist; available candidates are …" so it can regenerate. ② narrow tool granularity — split one big tool into small ones (never a universal get_record(id, field); use get_order(id) / get_customer(id) instead). Fewer arguments means less to hallucinate. Note: a tool description that spells out "when to call it" and "where the argument comes from" reduces misfires more than the schema itself.
What parse-failure rate should I expect, and how do I handle failures in production?
With frontier models plus structured outputs, parse failure is under 0.5% (mostly truncation on over-long outputs); pure prompt + JSON Mode runs 2–8% (15%+ on small models). Four-step defense chain: ① validate — pydantic/zod against the full schema, never just json.loads; ② automatic retry — feed the validation error plus the original input back to the model, at most 2 times, with the retry prompt stating exactly "your last output was missing field X / had the wrong type"; ③ truncation handling — set max_tokens with headroom (estimate the schema’s maximum length × 1.5); truncated JSON is unrecoverable and must be retried wholesale; ④ degradation path — if retries still fail, the business degrades (skip the step / route to a human / return a default) instead of retrying forever and burning money. Monitor the parse-failure rate on a dashboard; a 3× spike is an alert (usually a model version upgrade or a prompt change).
How do I design a schema the model can reliably follow?
Six iron rules: ① flatten — no more than 2 levels of nesting, no more than 8 fields per level (deep nesting is where small models collapse); ② prefer enums over open fields — if the value set is closed, make it an enum (status is ["pending","paid","refunded"], never free text); ③ self-explanatory names — business-semantic names (refund_reason, not code) plus a description on every field; ④ drive with examples — 1–2 complete few-shot output examples in the prompt beat the schema description alone; ⑤ minimize required — only core business fields are required, the rest optional with defaults; ⑥ no arrays of arrays — nested arrays are the reliability cliff of structured output; use a flat array plus a foreign key instead of a 2-D array.
How do I handle multilingual output and long-structure generation (e.g. a full report)?
Two high-frequency pitfalls: ① mixed-language fields — keep JSON field names and enum values fixed in English, and allow Chinese only in "content" fields (the schema description says "this field is in Chinese; all other values in English"). Otherwise downstream enum matching breaks: "已完成" never equals "completed". ② long-text truncation — cramming a whole report into one output field guarantees hitting max_tokens. The correct design is two-phase: call 1 emits a structured skeleton (section array + per-section outline); call 2..N fills each section (one call per section, input = skeleton + section requirements); the program assembles. Bonus: a failed section re-runs in isolation, not the whole document.
This article comes from AI Enable Harness front-line delivery practice. Need a similar system or optimization service?
Subscribe to Updates
Get notified when new articles are published. No spam, occasional updates only.
Subscribe →