Anyone who puts an LLM into a production system will, sooner or later, hit the same error in the middle of the night: JSON.parse blew up. Yesterday the model was dutifully returning structured data; today it suddenly wraps the output in an extra layer of markdown fencing, drops a closing brace, or just apologizes to you in prose. This article systematically maps out every known solution to this problem — each vendor's native capabilities (overseas and domestic), the principles and limits of constrained decoding, application-layer approaches in the TypeScript ecosystem, repair strategies, and production reliability engineering — and ends with a "layered defense" architecture you can copy directly.
response_format: json_object only guarantees that "the output is syntactically valid JSON" — it makes no guarantee whatsoever that the output conforms to any schema. OpenAI's own words: "Always use Structured Outputs instead of JSON mode whenever possible."json_schema + strict:true (beta, officially recommended but flagged as "use with caution in production"); the official APIs of DeepSeek, Tongyi Qianwen, and Zhipu GLM to this day offer only json_object, so the schema can only be written into the prompt and compliance rests on the model's goodwill.Every approach on the market sorts into three tiers by "where the guarantee comes from," and the difference in strength is mechanistic, not a matter of degree.
| Tier | Mechanism | What's guaranteed | Typical form |
|---|---|---|---|
| ① Prompt steering | Write the schema/examples into the prompt and rely on the model's goodwill | No guarantee. In OpenAI's internal eval, gpt-4-0613 scored only <40% compliance | schema-in-prompt, few-shot, prefill |
| ② JSON mode | Constrain the output to valid JSON syntax, without looking at the schema | Syntactic validity only; on truncation even this fails | json_object (DeepSeek/Qwen/GLM have only this tier) |
| ③ Constrained decoding | The schema is compiled into a grammar and illegal tokens are masked token by token | 100% structural compliance (truncation/refusal aside); value semantics are not guaranteed | OpenAI/Anthropic/Gemini/Ark strict, self-hosted XGrammar, etc. |
OpenAI's public description: it converts the submitted JSON Schema into a context-free grammar (CFG), and at each inference step uses the grammar to compute "the set of tokens legal at the current position," setting the sampling probability of illegal tokens to 0. The reason for choosing a CFG over a regex/FSM is that an FSM can't express recursive structures (matching brackets in deep nesting), whereas a CFG can support $ref: # recursive schemas. OpenAI officially credited open-source projects such as outlines, jsonformer, instructor, guidance, and lark as sources of inspiration.
The engineering cost is first-compile latency: the first request for a new schema requires preprocessing (OpenAI's 2024 figures: within 10 seconds for a typical schema, up to 1 minute for a complex one), after which cached reuse adds zero latency. Anthropic caches its compiled grammar artifact for 24 hours from last use, and the cache is invalidated as soon as the schema structure or the in-request tool set changes — meaning you should be restrained about schema versioning, because frequently changing the schema means continually paying the first-compile latency.
OpenAI's internal eval of compliance on a complex schema: pure prompt (gpt-4-0613) <40% → pure training improvements (gpt-4o-2024-08-06) 93% → the same model + constrained decoding 100%. OpenAI's own assessment: 93% "still does not meet the reliability that developers need to build robust applications." Note that 100% refers to structural validity only, not value correctness; that figure comes from the vendor's launch blog, and confidence is marked as medium.
All conclusions are based on the vendors' first-party docs, fetched live on 2026-07-07, with each item verified by 3-vote adversarial checking. Capabilities claimed by domestic vendors' third-party sites (SEO redirect pages / API resellers) were all re-verified against the official docs as the source of truth.
| Vendor / interface | Schema-level enforcement | Available modes | Key limits and pitfalls |
|---|---|---|---|
| OpenAI | Supported | json_schema strict (GA 2024-08) + legacy json_object |
Supports only a subset of JSON Schema (all fields required, additionalProperties:false, etc.); strict has compatibility limits with parallel function calling (recommend parallel_tool_calls:false); first-request compile latency |
| Anthropic | Supported (GA) | output_config.format (JSON outputs) + strict tool use (strict:true), usable independently or in combination |
Not supported: recursive schemas, min/max, minLength/maxLength, regex backreferences/lookahead; caps of 20 strict tools, 24 optional parameters, 16 anyOf, and exceeding them returns 400; the SDK automatically strips unsupported constraints and switches them to client-side validation (meaning numeric/length constraints are not enforced by the grammar); grammar cached 24h |
| Google Gemini | Supported | responseSchema (the Gemini 3 series uses responseJsonSchema); supports streaming structured output |
Unsupported schema keywords are silently ignored (versus the explicit 400 from others — a pitfall unique to Gemini); complex/deeply nested schemas may return 400; the docs explicitly require the application side to validate values: "schema-compliant but semantically incorrect" is listed as a routine failure mode |
| Volcano Ark (Doubao) | Supported, beta | json_schema + strict:true (officially positioned as the evolution of json_object and explicitly recommended) + json_object |
Official wording: "affected by resources and platform load, service availability may fluctuate with access conditions; please use with caution in production"; does not promise 100% correctness; limited to the models listed in the docs; under the Responses API the parameter name is text.format; the docs themselves recommend using Zod/Pydantic for client-side validation |
| DeepSeek | None | Only json_object (the API enum is just text / json_object) |
The prompt must contain the word "json" and give a format example; officially disclosed known issue: it may occasionally return empty content, and the only mitigation offered is to tweak the prompt → empty-content detection + retry must be built by you; workaround: the beta strict tool calling can enforce the schema of function arguments |
| Alibaba Cloud Bailian (Tongyi) | None | Only json_object (grep for json_schema across 5 official pages returns zero hits) |
The keyword "JSON" must appear in the System/User message or you get an outright 400 (corroborated by production incidents); the official production guide names Ajv validation + retry-on-failure/switch-model-to-repair; open-source Qwen weights can get grammar-level constraints on hosts like vLLM (a host capability, not a platform capability) |
| Zhipu GLM (Z.AI / bigmodel) | None | Only json_object (the API closes response_format down to a two-value enum) |
The schema must be written into the system prompt; the official docs write out a multi-layer defense prescription of their own: "multi-layer validation: schema validation + business-logic validation," "prepare a simplified backup schema," "log error details," and the official example catches both JSONDecodeError and ValidationError |
The three domestic vendors that have only json_object (DeepSeek/Tongyi/Zhipu) are precisely the ones whose official docs read most like a "defensive engineering guide" — which amounts to the vendors admitting, in documentation form, that json_object output cannot be trusted directly. This is exactly why application-layer defense can't be skipped no matter which vendor you integrate with.
This is a conclusion consistent across all four of OpenAI / Anthropic / Google / Ark, and all four docs provide a corresponding detection method — meaning these branches must be written for every vendor.
| Failure mode | Symptom | Detection method |
|---|---|---|
| max_tokens truncation | The output is incomplete, schema-violating JSON (even JSON mode's syntax guarantee fails along with it) | OpenAI: finish_reason === "length" (the Responses API uses status incomplete); Anthropic: stop_reason === "max_tokens" |
| Safety refusal | The refusal text takes priority over the schema constraint; still returns 200 and is billed as usual | OpenAI: a separate refusal field; Anthropic: stop_reason === "refusal" |
| Value-level / semantic errors | The structure is fully valid, but the field contents are wrong (constrained decoding fundamentally can't prevent this) | Business-rule validation (Zod refine), downstream consistency checks, evals |
| Schema not supported | You used a keyword outside the vendor's subset: either an explicit 400 (OpenAI/Anthropic/Ark), or silently ignored (Gemini) | Cross-check against the vendor's schema-support list before launch; Gemini needs dedicated testing of whether the constraint actually took effect |
| Beta availability fluctuation | Ark's json_schema is officially stated to fluctuate in availability with load | Keep a json_object fallback switch, roll out gradually |
"OpenAI Structured Outputs guarantees the output conforms to the schema, so you no longer need a validate-and-retry loop" — check it line by line against the four vendors' docs and you'll find that strict only eliminated the "schema violation" class of failure; truncation, refusals, and value-level errors still need an application-layer safety net, and OpenAI's own docs even provide official handling code for finish_reason and refusal. Any architecture premised on "once strict is on you can parse naked" rests on an assumption the vendor itself doesn't accept.
When you deploy an open-source model yourself, the schema-enforcement capability comes from the inference framework, not the model. This path can upgrade json_object-only open weights (Qwen, GLM, DeepSeek distillations) into grammar-level enforcement.
guided_json in v0.12.0, and the current parameter is structured_outputs; in the v0.10 era, enabling constrained decoding at batch ≥ 8 caused a significant performance drop, whereas SGLang overlaps CPU-side mask generation with GPU inference to nearly hide the overhead — how the framework integrates it is the decisive factor in self-hosting selection.You have to look at both sides of the benchmark data: on a repetitive schema (book-info), unconstrained tops out at only 72% structural accuracy, while both XGrammar and LLGuidance reach 100%; but in a dynamic-schema test (github_easy), XGrammar still produces 2.21% invalid JSON and LLGuidance 0.12%. Selection heuristic: repetitive fixed schema → XGrammar (reaping the grammar-cache dividend); dynamic complex schema → LLGuidance. Either way, the application-layer parse-and-validate safety net can't be removed.
Four representative solutions, each embodying one line of thinking: SDK abstraction, retry loop, fault-tolerant parsing, and types-as-schema.
output property on generateText/streamText (Output.object/array/choice/json), abstracting away the differences between vendors' structured outputs (generateObject is no longer the docs' primary recommendation).AI_NoObjectGeneratedError, carrying the raw text, response metadata, token usage, and the underlying cause — a ready-made monitoring hook.elementStream is the exception — each element is validated individually once complete.response_model on chat.completions.create, going through function calling under the hood (TOOLS mode, the default recommendation) rather than a naked JSON prompt.max_retries: on validation failure it automatically feeds the Zod validation error back into the next request for the model to fix, until it passes — including refine custom business rules. This is the benchmark implementation of the "validate-then-retry-with-error-feedback" pattern.It uses TypeScript type definitions as the schema, and on validation failure automatically repairs and retries carrying the type errors. Engineering-wise it has been surpassed by the ones above, but the paradigm of "the type system as the contract + a validate-and-repair loop" originates here.
Before re-calling the model (expensive, slow), deterministic repair (fast, free) should come first.
```json ... ```, so strip it before parsing; a more robust approach is to take the slice from the first { to the last }.JSONRepairError, which you can wire straight into a fallback branch. There's a streaming variant, jsonrepairTransform (default 64KB buffer; a single overly long string triggers Index out of range, so bump the buffer for long text fields).max_tokens should leave headroom for the worst case of your output schema; repairing truncated JSON should be only the last resort — finish_reason detection + a reasonable budget is the real fix.Even with strict on, don't delete this layer: it determines the quality of the "values," and it's your portability safeguard when migrating to json_object-only vendors.
messages must contain the word json in some form). Treat this as an API contract when writing prompt templates.{, forcing the model to continue from inside the JSON and naturally eliminating opening filler (a classic Anthropic technique; note that on Anthropic, prefill is incompatible with its structured output feature — pick one).reasoning field before the conclusion fields amounts to forcing "think first, answer later"; conversely, putting the answer first turns the reasoning field into an after-the-fact fabrication.jsonrepair → parse again. Zero cost, zero latency.Each layer only catches the failure modes the previous layer clearly missed; remove any layer, and that layer's corresponding failures reach the user directly.
async function callStructured<T>(schema: z.ZodType<T>, body: ChatBody): Promise<T> {
for (let attempt = 0; attempt <= MAX_RETRY; attempt++) {
const res = await chat(body); // L1: json_schema strict (Ark beta, auto-falls back to json_object on failure)
const choice = res.choices?.[0];
if (choice?.finish_reason === "length") { metric("llm_truncated"); continue; } // L2: truncation gate
const raw = choice?.message?.content ?? "";
if (!raw.trim()) { metric("llm_empty"); continue; } // L2: empty content (DeepSeek known issue)
let parsed: unknown;
try { parsed = JSON.parse(stripFences(raw)); }
catch { try { parsed = JSON.parse(jsonrepair(raw)); } // L3: deterministic repair
catch { metric("llm_unparseable", { raw }); continue; } }
const v = schema.safeParse(parsed); // L2: schema + refine business rules
if (v.success) return v.data;
body = withErrorFeedback(body, raw, v.error); // L3: corrective retry (Instructor pattern)
metric("llm_schema_fail", { issues: v.error.issues });
}
throw new StructuredOutputError(); // caller wires L3 safety net: cache last-good / degrade & hide
}
All vendor-capability conclusions were verified against official documentation via live fetch on 2026-07-07; third-party claims (such as SEO sites saying "DeepSeek/GLM already support json_schema") were verified to be false or non-official capabilities.