← Research Notes中文
Technology · 2026-07-07 · 20 min read · Deep study

How to Make Sure an LLM Returns JSON That Strictly Matches the Expected Format

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.

Every conclusion about vendor capabilities in this article has been verified against each vendor's current official documentation, fetched on 2026-07-07, with false claims from third-party SEO sites stripped out.

01Core Conclusions

02Three Tiers of Guarantee Strength & How Constrained Decoding Works

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.

TierMechanismWhat's guaranteedTypical 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.

How constrained decoding works

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.

Key numbers

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.

03Vendor Capability Comparison

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 / interfaceSchema-level enforcementAvailable modesKey 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
Worth savoring

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.

04Residual Failures That Even strict Mode Can't Escape

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 modeSymptomDetection 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
A widely repeated claim that doesn't hold up

"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.

05Self-Hosted Constrained Decoding

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.

Self-hosting is not failure-free either

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.

06The TypeScript Application-Layer Ecosystem

Four representative solutions, each embodying one line of thinking: SDK abstraction, retry loop, fault-tolerant parsing, and types-as-schema.

Vercel AI SDK — the SDK abstraction layer

Instructor-JS — a retry loop with error feedback

BAML (Schema-Aligned Parsing) — the fault-tolerant parsing route

TypeChat (Microsoft) — the pioneer of types-as-schema

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.

07Parsing & Repair Strategies

Before re-calling the model (expensive, slow), deterministic repair (fast, free) should come first.

08Prompt-Layer Techniques

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.

09Production Reliability Engineering

The recovery ladder (cheapest first)

  1. Deterministic repair: fence stripping → jsonrepair → parse again. Zero cost, zero latency.
  2. Retry as-is: the same request + exponential backoff with jitter (reference implementation: up to 3 times). Many failures are transient and clear up on the second try.
  3. Corrective retry: feed the failed output and the precise parser/Zod error message back into the next request, restate the schema, and let the model fix its mistake (the Instructor pattern). A finite number of times, usually 1–2.
  4. Fallback: switch to a simplified backup schema (Zhipu's official prescription) / switch to another model to repair the output (Alibaba Cloud's official prescription).
  5. Business safety net: the last good result (cache), default content, or silently hiding the feature — never dump a 500 on the user.

Observability

Parameters and versions

10Frontiers & Debates

11A Layered-Defense Reference Architecture

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.

L0 Prompt layer
Write the schema + example into the prompt, including the "json" keyword Arrange field order so reasoning comes first, conclusions later; use low temperature for extraction-type tasks. Catches: value quality, cross-vendor portability (DeepSeek/Qwen/GLM have only this layer available)
L1 Request layer
Turn on constrained decoding wherever you can: json_schema + strict:true Design the schema against the vendor's schema-subset list; leave headroom in max_tokens for the worst case; put a fallback switch on beta capabilities and roll out gradually. Catches: structural violations (from "probably right" to "mechanism-guaranteed")
L2 Gate layer
Check finish_reason / empty content before parsing, and after parsing it must pass Zod safeParse Attach business rules via refine on the same schema; don't validate partial streaming objects, only validate complete ones. Catches: truncation, refusals, empty content, value-level errors (everything strict mode explicitly doesn't handle)
L3 Recovery layer
Repair → retry as-is → retry with error feedback → fallback → business safety net Deterministic repair (jsonrepair) first; corrective retry limited to 1–2 times; keep a simplified schema or backup model ready; the final safety net is cache/default values, not a 500. Catches: transient failures, stubborn format drift, vendor availability fluctuation
L4 Observability layer
Failure rate / truncation rate / retry rate / fallback rate, tagged per endpoint × schema version + alerting Retain raw output on failure; version the schema; run a fixed test suite for regression before switching models. It spans L0–L3 and is the only proof that the other four layers are effective.

Minimal implementation skeleton (Bun / TypeScript)

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
}

12Primary Sources

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.