LeetLLM
My PlanLearnGlossaryTracksPracticeBlog
LeetLLM

Your go-to resource for mastering AI & LLM systems.

Product

  • Learn
  • Glossary
  • Tracks
  • Practice
  • Blog
  • RSS

Legal

  • Terms of Service
  • Privacy Policy

© 2026 LeetLLM. All rights reserved.

All Topics
Your Progress
0%

0 of 177 articles completed

🛠️Computing Foundations0/9
Git, Shell, Linux for AIDocker for Reproducible AIPython for AI EngineeringNumPy and Tensor ShapesCUDA for ML TrainingMPS & Metal for ML on MacData Structures for AISQL and Data ModelingAlgorithms for ML Engineers
📊Math & Statistics0/8
Gradients and BackpropVectors, Matrices & TensorsLinear Algebra for MLAdam, Momentum, SchedulersProbability for Machine LearningStatistics and UncertaintyDistributions and SamplingHypothesis Tests, Intervals, and pass@k
📚Preparation & Prerequisites0/13
Neural Networks from ScratchCNNs from ScratchTraining & BackpropagationSoftmax, Cross-Entropy & OptimizationRNNs, LSTMs, GRUs, and Sequence ModelingAutoencoders and VAEsThe Transformer Architecture End-to-EndLanguage Modeling & Next TokensFrom GPT to Modern LLMsPrompt Engineering FundamentalsCalling LLM APIs in ProductionFirst AI App End-to-EndThe LLM Lifecycle
🧮ML Algorithms & Evaluation0/11
Linear Regression from ScratchLogistic Regression and MetricsDecision Trees, Forests, and BoostingReinforcement Learning BasicsValidation and LeakageClustering and PCACore Retrieval AlgorithmsDecoding AlgorithmsExperiment Design and A/B TestingPyTorch Training LoopsDataset Pipelines and Data Quality
📦Production ML Systems0/6
Feature Engineering for Production MLBatch and Streaming Feature PipelinesGradient Boosted Trees in ProductionRanking and Recommendation SystemsForecasting and Anomaly DetectionMonitoring Predictive Models
🧪Core LLM Foundations0/8
The Bitter Lesson & ComputeBPE, WordPiece, and SentencePieceStatic to Contextual EmbeddingsPerplexity & Model EvaluationFile Ingestion for AIChunking StrategiesLLM Benchmarks & LimitationsInstruction Tuning & Chat Templates
🧰Applied LLM Engineering0/24
Dimensionality Reduction for EmbeddingsCoT, ToT & Self-Consistency PromptingFunction Calling & Tool UseMCP & Tool Protocol StandardsContext EngineeringPrompt Injection DefenseResponsible AI GovernanceData Labeling and Human FeedbackEvaluating AI AgentsProduction RAG PipelinesHybrid Search: Dense + SparseReranking and Cross-Encoders for RAGRAG Evaluation for Reliable AnswersLLM-as-a-Judge EvaluationBias & Fairness in LLMsHallucination Detection & MitigationLLM Observability & MonitoringExperiment Tracking with MLflow and W&BPrompt Optimization with DSPyModel Versioning & DeploymentSemantic Caching & Cost OptimizationLLM Cost Engineering & Token EconomicsModel Gateways, Routing, and FallbacksDesign an Automated Support Agent
🎓Portfolio Capstones0/9
Capstone: Delivery ETA PredictionCapstone: Product RankingCapstone: Demand ForecastingCapstone: Image Damage ClassifierCapstone: Production ML PipelineCapstone: Document QACapstone: Eval DashboardCapstone: Fine-Tuned ClassifierCapstone: Reproducible ML Study
🧠Transformer Deep Dives0/8
Sentence Embeddings & Contrastive LossEmbedding Similarity & QuantizationScaled Dot-Product AttentionVision Transformers and Image EncodersPositional Encoding: RoPE & ALiBiLayer Normalization: Pre-LN vs Post-LNMechanistic InterpretabilityDecoding Strategies: Greedy to Nucleus
🧬Advanced Training & Adaptation0/16
Scaling Laws & Compute-Optimal TrainingPre-training Data at ScaleBuild GPT from Scratch LabJAX for PyTorch ResearchersContinued Pretraining for Domain ShiftSynthetic Data PipelinesSupervised Fine-Tuning PipelineMixed Precision TrainingDistributed Training: FSDP & ZeROLoRA & Parameter-Efficient TuningReward Modeling from Preference DataRLHF & DPO AlignmentConstitutional AI & Red TeamingRLVR & Verifiable RewardsKnowledge Distillation for LLMsModel Merging and Weight Interpolation
🤖Advanced Agents & Retrieval0/16
Vector DB Internals: HNSW & IVFAdvanced RAG: HyDE & Self-RAGGraphRAG & Knowledge GraphsRAG Security & Access ControlStructured Output GenerationReAct & Plan-and-ExecuteGuardrails & Safety FiltersCode Generation & SandboxingComputer-Use / GUI / Browser AgentsHuman-in-the-Loop Agent ArchitectureAI Coding Workflow with AgentsAgent Memory & PersistenceAgent Failure & RecoveryRecursive Language Models (RLM)Multi-Agent OrchestrationCapstone: Production Agent
⚡Inference & Production Scale0/19
Inference: TTFT, TPS & KV CacheMulti-Query & Grouped-Query AttentionKV Cache & PagedAttentionPrefix Caching and Prompt CachingFlashAttention & Memory EfficiencyContinuous Batching & SchedulingScaling LLM InferenceModel Parallelism for LLM InferenceModel Quantization: GPTQ, AWQ & GGUFLocal LLM DeploymentSLM Specialization & Edge DeploymentSpeculative DecodingLong Context Window ManagementMixture of Experts ArchitectureMamba & State Space ModelsReasoning & Test-Time ComputeAdvanced MLOps & DevOps for AIGPU Serving & AutoscalingA/B Testing for LLMs
🏗️System Design Capstones0/9
Content Moderation SystemCode Completion SystemMulti-Tenant LLM PlatformLLM-Powered Search EngineVision-Language Models & CLIPMultimodal LLM ArchitectureDiffusion Models: Images & TextReal-Time Voice AI AgentReasoning & Test-Time Compute
🎤AI Lab Interviewing0/4
AI Lab Coding Interview: Python SystemsAI Lab System Design InterviewAI Lab Behavioral InterviewAI Lab Technical Presentation
🔬Project Deep Dives0/17
Deep Dive - vLLMDeep Dive - SkyRLDeep Dive - FlashAttentionDeep Dive - FlashInferDeep Dive - DeepGEMMDeep Dive - NCCLDeep Dive - MegatronDeep Dive - DeepSpeedDeep Dive - RayDeep Dive - MLflowDeep Dive - PyTorchDeep Dive - TransformersDeep Dive - SGLangDeep Dive - slimeDeep Dive - DeepEPDeep Dive - TinkerDeep Dive - Light-PEFT
Back to Topics
LearnAdvanced Agents & RetrievalStructured Output Generation
🤖HardLLM Agents & Tool Use

Structured Output Generation

Build reliable LLM interfaces with JSON mode, structured outputs, schema validation, and grammar-guided decoding.

37 min read
Learning path
Step 117 of 177 in the full curriculum
RAG Security & Access ControlReAct & Plan-and-Execute

Personalize this lesson

Adapt explanations and teaching visuals to your background and preferred voice.

A secure RAG prompt controls which documents enter the model. Structured output controls what leaves it: typed data that downstream code can parse, validate, and route without fragile string cleanup.

Structured output generation turns free-form model text into typed records that software can validate and route. Schemas, constrained decoding, and explicit recovery paths matter whenever LLM output feeds another system.

A CI triage pipeline that processes build logs automatically needs typed fields. A developer writes, "Run RUN-842 failed in unit-tests after auth-cache timed out." Your pipeline needs to extract the run ID and failure type so it can route the incident to the right workflow and look up the run in CI.

You ask an LLM to extract this information and return JSON. If the model replies with, "Extracted data: { 'run_id': 'RUN-842', 'failure_type': 'test_failure' }", your parser crashes because of the preamble text. That single failure blocks an entire automation pipeline.

When LLM output feeds software, it must be machine-parseable and validated. A missing comma, an unexpected field name, or a polite introduction can break downstream processing. Structured output generation is the set of techniques that make model outputs conform to a defined schema, replacing fragile string cleanup.

The practical range runs from best-effort JSON mode to token-level constraint enforcement via grammar-guided decoding [1]Reference 1Efficient Guided Generation for Large Language Models.https://arxiv.org/abs/2307.09702, open-source runtimes like Outlines and SGLang [2]Reference 2Outlines Documentationhttps://dottxt-ai.github.io/outlines/latest/[3]Reference 3SGLang: Efficient Execution of Structured Language Model Programs.https://arxiv.org/abs/2312.07104[4]Reference 4SGLang Structured Outputs Documentationhttps://docs.sglang.io/docs/advanced_features/structured_outputs, and hosted patterns using OpenAI's Structured Outputs feature.[5]Reference 5Structured outputshttps://developers.openai.com/api/docs/guides/structured-outputs

Constrained decoding trace where competing next-token proposals hit a grammar mask, illegal tokens drop out, and only one legal JSON path continues.
Grammar-guided decoding changes the sampler itself. Invalid next tokens disappear before sampling, so only legal structure can continue.

The illustration above shows how a grammar engine filters the model's next-token probabilities. Invalid tokens (like a word when a number is required) are masked to negative infinity, so the sampler can only pick grammar-compliant tokens.

When does structured output matter most?

Answer

Use it whenever model output feeds software: parsers, queues, databases, tool calls, eval pipelines, or dashboards. If a human is reading the answer directly, free-form text may be better. If code consumes the answer, the model needs a contract.

Why "please return JSON" isn't enough

Before looking at solutions, watch how naive prompting fails. Suppose you send this prompt to a chat model:

why-please-return-json-is-not-enough.py
1prompt = """ 2Extract run_id and failure_type from this build log: 3"Run RUN-842 failed in unit-tests after auth-cache timed out." 4Return JSON. 5"""

Three real failures look like this:

Failure modeExample outputWhy it breaks
Preamble text"Here is the JSON: { 'run_id': 'RUN-842' }"The parser sees Here before the brace and throws
Markdown wrapper"json\n{ 'run_id': 'RUN-842' }\n"The triple backticks and newlines aren't valid JSON
Wrong shape{ "run_id": "RUN-842", "failure_type": null }Your database expects failure_type to be a string, not null

None of these are model "hallucinations" in the usual sense. The model followed a weak natural-language request. Your request was underspecified. You asked for JSON, but you didn't enforce JSON.

Move from asking to enforcing. The enforcement tiers run from weakest to strongest.

Why does "return JSON" fail as an interface contract?

Answer

It's a request written in natural language, not a parser-enforced contract. The model may add preamble text, wrap JSON in Markdown, use single quotes, omit fields, or choose a shape your application doesn't accept.

Contract choices and enforcement mechanisms

Engineers need to choose both the contract exposed to application code and the mechanism that enforces it. These choices aren't a strict ladder: a hosted structured-output API may enforce a JSON Schema using constrained decoding internally, while a self-hosted runtime may expose grammar-guided decoding directly. Production machine-consumed output needs a checked contract, regardless of where enforcement runs.

ChoiceStructural contractTypical implementation
Prompt-based ("respond in JSON")None; best-effort formatting onlyMost chat APIs
JSON modeValid JSON syntax on successful completion, with edge cases to handleOpenAI JSON mode, similar API flags
Strict function callingSchema-constrained arguments for an action requestTool-calling APIs and agent runtimes
Structured-output APISchema adherence for supported schemasOpenAI Structured Outputs, schema-aware SDKs
Grammar-guided runtimeValid path through a supplied grammar or schema translationOutlines, SGLang, llama.cpp
A structured-output ladder comparing prompt-only formatting, JSON mode, structured-output APIs, and grammar runtimes using exact worked examples.
These tiers differ in where structure gets enforced. Prompts ask, JSON mode guarantees syntax, structured APIs check supported schema rules, and grammar runtimes block invalid token paths during sampling.

To see how these differ in practice, imagine the same CI failure log. Each tier changes where enforcement happens:

Prompt-based: The model might return valid JSON. It might return a monologue. You need a regex to clean the output, then a JSON parser, then a schema validator, then a retry loop. This is best-effort formatting, not enforcement.

JSON mode: The API constrains successful completions to valid JSON syntax. You still need to explicitly tell the model to produce JSON, detect incomplete outputs, and validate the shape yourself. JSON mode could still return {"foo": "bar"} when you wanted {"run_id": "string", "failure_type": "string"}. JSON mode is syntax enforcement, not schema enforcement.[5]Reference 5Structured outputshttps://developers.openai.com/api/docs/guides/structured-outputs

Structured-output APIs: The API takes a JSON Schema or Pydantic model and, when the request completes without refusal or truncation, returns output that matches supported schema features. The fields, types, and required properties are enforced within that supported subset.[5]Reference 5Structured outputshttps://developers.openai.com/api/docs/guides/structured-outputs

Grammar-guided decoding: A runtime prevents the sampler from picking tokens that would break its compiled grammar. At every step, invalid tokens have their probabilities set to zero. This describes an enforcement mechanism inside generation, not a stronger semantic guarantee: a grammar-valid payload can still contain wrong values.[1]Reference 1Efficient Guided Generation for Large Language Models.https://arxiv.org/abs/2307.09702

What is the difference between JSON mode and structured outputs?

Answer

JSON mode aims for valid JSON syntax. Structured outputs aim for a supported schema: required fields, object shapes, field types, and closed objects when configured. You still need to handle refusals, truncation, unsupported schema features, and semantic validation.

This executable comparison demonstrates the remaining gap after JSON syntax succeeds: an object can parse correctly and still fail its application schema.

json-syntax-versus-schema-contract.py
1import json 2 3from pydantic import BaseModel, ConfigDict, ValidationError 4 5class RunTriage(BaseModel): 6 model_config = ConfigDict(extra="forbid") 7 8 run_id: str 9 failure_type: str 10 11syntax_valid_but_wrong_shape = json.loads('{"foo": "bar"}') 12schema_valid = json.loads('{"run_id": "RUN-842", "failure_type": "test_failure"}') 13 14try: 15 RunTriage.model_validate(syntax_valid_but_wrong_shape) 16except ValidationError: 17 print("JSON-only result: rejected by schema") 18 19print("structured record:", RunTriage.model_validate(schema_valid).model_dump())
Output
1JSON-only result: rejected by schema 2structured record: {'run_id': 'RUN-842', 'failure_type': 'test_failure'}

The parser-gate analogy

Source code moving through a compiler parser with no grammar might be a valid statement, or it might be text that only looks close. This is prompt-based generation: you ask the model to "please produce JSON," but nothing stops it from outputting a monologue instead.

Grammar-guided decoding is like a parser gate during compilation. The grammar prevents the output from taking an invalid syntax path. In the context of an LLM, the gate is a finite state machine (FSM, a computational model that tracks transitions between allowed states) that monitors generation. Once the parser state expects a non-negative integer for retry_count, the FSM blocks letter tokens and any other token prefix that can't extend a legal integer. The decoder can't select a letter token that violates the grammar, the same way a compiler parser rejects a malformed statement.

What does grammar-guided decoding enforce?

Answer

It enforces structure during sampling by allowing only tokens that keep the output inside the grammar. It doesn't prove that extracted values are true, current, safe, or useful.

How grammar-guided decoding works

To understand how runtimes enforce structural compliance, start by examining the token generation process itself. By intercepting the token sampling phase, the runtime restricts selection to tokens that match the desired schema. Grammar constraints act as a filter during token generation:

Diagram showing LLM proposes next token, Grammar state, Sample valid token, and Mask invalid to -∞.
LLM proposes next token, Grammar state, Sample valid token, and Mask invalid to -∞.

Grammar-guided enforcement operates at the token sampling level. It transforms a schema or grammar into constraints that modify the model's output probabilities during generation.

From schema to logit masking

  1. Schema Compilation: For regular languages such as Regex, the runtime can compile the allowed strings into a Deterministic Finite Automaton (DFA) (a state machine where the next state is uniquely determined by the current state and input symbol). For general JSON schemas, especially nested or recursive ones, many systems instead compile to a context-free grammar or another stack-aware parser representation.
    • Example Schema: {"type": "object", "properties": {"age": {"type": "integer", "minimum": 0}}}
    • Simplified regular-expression equivalent: \{"age":\s*(0|[1-9][0-9]*)\}
  2. State Tracking: As the model generates tokens, the engine tracks the current DFA state or parser stack for the active grammar.
  3. Logit Masking: At each step, the engine identifies which tokens are valid transitions.
    • Scenario: The model has generated {"age": .
    • Valid Next Characters / Bytes: 1, 2, 3, ... 9, 0, (space).
    • Invalid Next Characters / Bytes: ", a, b, {, [, etc.
    • Action: The runtime maps those allowed prefixes back to token IDs, then sets the logits (unnormalized probabilities) of all invalid token IDs to −∞-\infty−∞. When softmax is applied, their probability becomes 0.

The toy code below shows the core masking idea without requiring a model checkpoint. The grammar state says that only { is legal as the next token, so every other candidate is assigned negative infinity before sampling:

from-schema-to-logit-masking.py
1def mask_invalid_tokens(logits: dict[str, float], valid_tokens: set[str]) -> dict[str, float]: 2 return { 3 token: score if token in valid_tokens else float("-inf") 4 for token, score in logits.items() 5 } 6 7next_token_logits = { 8 "The": 0.45, 9 "{": 0.30, 10 "Sure": 0.15, 11 "JSON": 0.10, 12} 13 14masked = mask_invalid_tokens(next_token_logits, valid_tokens={"{"}) 15chosen = max(masked, key=masked.get) 16valid_after_mask = [token for token, score in masked.items() if score != float("-inf")] 17print("valid after mask:", valid_after_mask) 18print("chosen token:", chosen)
Output
1valid after mask: ['{'] 2chosen token: {

This enforces structural adherence to the allowed grammar as long as generation can continue normally. In production, you still need to handle refusals, truncation, and semantic mistakes in the returned values.[5]Reference 5Structured outputshttps://developers.openai.com/api/docs/guides/structured-outputs

Why can a schema-valid result still be wrong?

Answer

The grammar can require { "run_id": "...", "status": "..." }, but it can't check your CI database, job logs, or deployment policy. It controls format; your application still validates meaning.

The tokenizer alignment problem

The clean DFA story above hides the hardest systems detail. Grammars are usually written over characters or bytes, but LLMs don't sample characters. They sample subword tokens. That tokenization boundary means the runtime must answer a much harder question than "is } valid here?" It must answer "which of the 100,000+ vocabulary items could extend some valid character prefix from this state?" [1]Reference 1Efficient Guided Generation for Large Language Models.https://arxiv.org/abs/2307.09702

That token-to-grammar alignment step is where a lot of the real engineering work lives. Outlines precomputes an index used during guided generation, while SGLang's paper explicitly calls out compressed finite state machines for faster structured decoding.[1]Reference 1Efficient Guided Generation for Large Language Models.https://arxiv.org/abs/2307.09702[3]Reference 3SGLang: Efficient Execution of Structured Language Model Programs.https://arxiv.org/abs/2312.07104 If an implementation does this naively, masking can become a noticeable part of per-token latency.

Hosted APIs can hide some of that machinery. OpenAI's current docs include a general first-schema latency caveat for Structured Outputs: the first request with a schema can add processing latency, while later requests with the same schema avoid that extra work. The same page also notes a fine-tuned-model-specific version of that caveat for response_format, where other models don't have that particular limitation.[5]Reference 5Structured outputshttps://developers.openai.com/api/docs/guides/structured-outputs

Why does tokenizer alignment make constrained decoding harder than parsing characters?

Answer

Schemas are usually written over characters or bytes, but models sample vocabulary tokens. The runtime has to map "which strings are valid next" into "which token IDs can still lead to a valid string" at every generation step.

Regex vs. context-free grammars

When working with flat data structures, regular expressions (Regex) compiled to DFAs work well. But what if your JSON object contains object lists or several nesting levels?

Regular-language constraints can't represent arbitrarily nested structures like general JSON. To enforce nested or recursive schemas, engines need a Context-Free Grammar (CFG) or another stack-aware parser representation. Such representations track open brackets [ and { until matching closing brackets ] and }. Modern libraries handle translation automatically; benchmark nested schemas because parser state and output size can affect latency.

When is a regular expression enough, and when do you need a CFG?

Answer

Regex is enough for flat patterns like a run ID, a bounded enum, or one shallow object shape. Nested JSON, arrays of objects, recursive trees, and balanced brackets need a stack-aware grammar or parser representation.

Open-source engines: Outlines, llama.cpp, SGLang, and XGrammar

While hosted Structured Outputs APIs are convenient, open-source runtimes give you direct control over constrained decoding for self-hosted models. For teams deploying open-weight models, managed API features may not fit. Instead, they use inference engines or libraries that support constrained generation natively and can cache compiled grammars or shared prefixes inside the serving stack.

Outlines

Outlines [1]Reference 1Efficient Guided Generation for Large Language Models.https://arxiv.org/abs/2307.09702[2]Reference 2Outlines Documentationhttps://dottxt-ai.github.io/outlines/latest/ provides a high-level Python interface for structured generation. In current releases, you build an Outlines model once, then create a reusable Generator for a hot target type. That keeps the model wrapper, tokenizer, and schema-bound generation machinery off the per-request setup path.

This is an integration example, not a no-dependency local script. It requires outlines, transformers, pydantic, a compatible model backend, and enough local compute to load the chosen model.

outlines.py
1from typing import Literal 2 3import outlines 4from outlines import Generator 5from pydantic import BaseModel 6from transformers import AutoModelForCausalLM, AutoTokenizer 7 8class Character(BaseModel): 9 name: str 10 role: Literal["Warrior", "Mage", "Rogue"] 11 level: int 12 13model_name = "microsoft/Phi-3-mini-4k-instruct" 14model = outlines.from_transformers( 15 AutoModelForCausalLM.from_pretrained(model_name, device_map="auto"), 16 AutoTokenizer.from_pretrained(model_name), 17) 18 19character_generator = Generator(model, Character) 20raw = character_generator( 21 "Create a level-5 fantasy RPG character.", 22 max_new_tokens=120, 23) 24character = Character.model_validate_json(raw)

Enums and required fields can be enforced during decoding, but business rules still belong in post-validation. For example, if level must be between 1 and 100, validate that in your application even if the grammar already narrows the shape.

What should you build once at startup in an Outlines-style integration?

Answer

Build the model wrapper, tokenizer/backend connection, and reusable generators for hot schemas once where possible. Keep per-request work focused on the prompt so schema compilation and tokenizer setup don't sit on every request's critical path.

llama.cpp

llama.cpp exposes grammar constraints directly through GBNF and can also convert a subset of JSON Schema into grammars for its server and CLI flows.[6]Reference 6llama.cpp: Inference of LLaMA model in pure C/C++https://github.com/ggml-org/llama.cpp[7]Reference 7llama.cpp Grammars Documentationhttps://github.com/ggml-org/llama.cpp/blob/master/grammars/README.md One subtle but important detail from its docs is that the schema is used to constrain decoding, not automatically to teach the model what the fields mean. For plain structured generation, you still want prompt instructions that explain the task and the semantics of the fields you're asking for.[7]Reference 7llama.cpp Grammars Documentationhttps://github.com/ggml-org/llama.cpp/blob/master/grammars/README.md

Why does a constrained decoder still need task instructions?

Answer

The grammar can say a field named status must be a string. It doesn't teach whether "failed" or "passed" is the correct value for RUN-842. The prompt explains the task semantics; the grammar enforces the output shape.

SGLang

SGLang (Structured Generation Language) [3]Reference 3SGLang: Efficient Execution of Structured Language Model Programs.https://arxiv.org/abs/2312.07104[4]Reference 4SGLang Structured Outputs Documentationhttps://docs.sglang.io/docs/advanced_features/structured_outputs goes further by optimizing the runtime for structured workloads. Its paper describes two separate ideas that are easy to conflate:

  1. RadixAttention reuses KV cache for shared token prefixes across requests.
  2. Compressed finite state machines speed up structured decoding itself.[3]Reference 3SGLang: Efficient Execution of Structured Language Model Programs.https://arxiv.org/abs/2312.07104

Under the hood, SGLang stores token-prefix mappings in a radix tree and reuses previously computed KV cache when a new request shares a prompt prefix.[3]Reference 3SGLang: Efficient Execution of Structured Language Model Programs.https://arxiv.org/abs/2312.07104 That helps Time To First Token (TTFT) when many requests reuse the same system prompt, few-shot examples, or schema instructions. It's a prefix-caching optimization, not a grammar-state cache.

SGLang also provides a domain-specific language (DSL, a specialized syntax for a particular application domain) for interleaving Python control flow with LLM generation. Its structured-output documentation exposes JSON Schema, regex, and grammar constraints; its runtime design separately addresses cache reuse.[4]Reference 4SGLang Structured Outputs Documentationhttps://docs.sglang.io/docs/advanced_features/structured_outputs[3]Reference 3SGLang: Efficient Execution of Structured Language Model Programs.https://arxiv.org/abs/2312.07104

Why should you separate prefix caching from grammar-state optimization?

Answer

Prefix caching saves prefill work when prompts share text. Grammar-state optimization makes valid-token computation cheaper during decoding. Both reduce latency, but they act on different parts of the serving path.

XGrammar

XGrammar [8]Reference 8XGrammar: Flexible and Efficient Structured Generation Engine for Large Language Modelshttps://arxiv.org/abs/2411.15100 is a grammar engine built specifically to make context-free-grammar decoding cheap enough for production serving. It attacks the tokenizer-alignment cost from the previous section head on. It splits the vocabulary into context-independent tokens, whose validity can be precomputed regardless of the parser stack, and context-dependent tokens, which must be checked at runtime against the current stack. A persistent stack and overlap with GPU execution then shrink the per-token mask cost further.[8]Reference 8XGrammar: Flexible and Efficient Structured Generation Engine for Large Language Modelshttps://arxiv.org/abs/2411.15100

The paper reports up to 100x faster grammar processing than evaluated prior approaches and near-zero structured-generation overhead in its end-to-end serving experiments.[8]Reference 8XGrammar: Flexible and Efficient Structured Generation Engine for Large Language Modelshttps://arxiv.org/abs/2411.15100 Serving stacks can expose engines such as XGrammar behind a higher-level structured-output interface; verify which backend and benchmark settings your deployment uses.

How does XGrammar make grammar-guided decoding cheaper?

Answer

It precomputes validity for context-independent tokens, checks only context-dependent tokens at runtime against a persistent parser stack, and overlaps grammar computation with GPU work. That keeps per-token masking close to free in end-to-end serving.

Function calling vs. structured outputs

Both techniques constrain model outputs, but they solve different orchestration problems in an AI system. It's common to blur them together because both can involve JSON schemas. Some APIs also allow strict tool-argument schemas, but the core distinction still holds: function calling is about selecting actions, while structured outputs are about returning data in a fixed contract.

AspectFunction CallingStructured Outputs
Primary GoalAction Selection (Tool Use)Data Extraction / Formatting
TriggerApplication may allow or require a tool call; model supplies argumentsApplication requests a format for the returned record
OutputFunction name + argumentsArbitrary JSON object
Control FlowLoop: Model -> Code -> ModelLinear: Model -> Parser -> App

When should you choose function calling instead of structured outputs?

Answer

Choose function calling when the application needs an action request and structured arguments, whether tool choice is automatic or forced. Choose structured outputs when every response should be a terminal data record for your code to parse.

When to use function calling

Use function calling when the model is an agent that needs to interact with the world. In a typical tool loop, the application exposes allowed tools and may let the model choose one or require a specific call. The application executes an accepted call, then returns its result to the model in a subsequent turn. This allows the agent to formulate a final user-facing response based on fetched data.

This example defines a tool for an agent to fetch CI run status. If tool choice is left automatic, the model can either call the function with structured arguments or return a normal text response:

when-to-use-function-calling.py
1tools = [ 2 { 3 "type": "function", 4 "name": "get_run_status", 5 "description": "Fetch current CI status for a run.", 6 "strict": True, 7 "parameters": { 8 "type": "object", 9 "properties": { 10 "run_id": {"type": "string"} 11 }, 12 "required": ["run_id"], 13 "additionalProperties": False 14 } 15 } 16] 17 18tool_schema = tools[0] 19example_routes = { 20 "What happened to RUN-842?": "get_run_status", 21 "Hi!": "text_response", 22} 23print("tool:", tool_schema["name"]) 24print("strict:", tool_schema["strict"]) 25print("routes:", example_routes)
Output
1tool: get_run_status 2strict: True 3routes: {'What happened to RUN-842?': 'get_run_status', 'Hi!': 'text_response'}

If your provider supports strict tool schemas, enable them. That improves argument reliability, but the control flow is still tool invocation rather than terminal data extraction. On OpenAI, manual strict tool schemas should set strict: true, list every parameter in required, and close objects with additionalProperties: false. parallel_tool_calls=false is useful when your application expects zero or one call, but that's an orchestration choice, not the definition of strictness.[5]Reference 5Structured outputshttps://developers.openai.com/api/docs/guides/structured-outputs

Why does a strict tool schema not replace authorization checks?

Answer

It can make the tool arguments well shaped. It doesn't know whether the user can access the repository, whether the action is allowed, or whether the tool should run. Authorization, limits, and approval gates still belong in application code.

When to use structured outputs

Use structured outputs when you need to extract data or create a reliable interface between the LLM and your code. Unlike the multi-turn loop of function calling, structured outputs are typically terminal, single-turn actions used purely for data formatting and strict schema adherence.

This example defines a Pydantic model for a run-status update and passes it directly to OpenAI's parsing helper. The SDK handles the JSON schema conversion for you, and output_parsed gives you a typed object back when the model succeeds.[5]Reference 5Structured outputshttps://developers.openai.com/api/docs/guides/structured-outputs This snippet requires the OpenAI Python SDK and an OPENAI_API_KEY.

when-to-use-structured-outputs.py
1from openai import OpenAI 2from pydantic import BaseModel 3 4client = OpenAI() 5 6class RunStatusUpdate(BaseModel): 7 run_id: str 8 status: str 9 failing_job: str 10 11response = client.responses.parse( 12 model="gpt-4o-mini", 13 input="Run RUN-842 failed in unit-tests.", 14 text_format=RunStatusUpdate, 15) 16 17update = response.output_parsed

One provider-specific gotcha: hosted structured-output APIs usually implement a subset of JSON Schema, not the full spec. On OpenAI, that means root-level anyOf isn't allowed, every field must be required, and every object must opt into closed-world generation with additionalProperties: false.[5]Reference 5Structured outputshttps://developers.openai.com/api/docs/guides/structured-outputs Treat schema design as part of your API integration, not as a prompt-writing detail alone.

What does additionalProperties: false protect you from?

Answer

It closes the object shape so the model can't add surprise fields like admin_note, debug_prompt, or raw_user_text. That makes downstream code safer because unknown keys are rejected instead of silently accepted.

Typed record moves through schema gate and semantic gate. Clean output continues to acceptance, while bad shape or bad real-world meaning stops on separate reject branches.
Schema-valid JSON is only first gate. Shape passes first, then application code checks ownership, policy, and source-of-truth state before anything downstream trusts it.

The next example keeps these gates separate. Pydantic checks shape and enum values; application code checks whether the authenticated engineer can access the run's repository.

separate-format-from-semantic-validation.py
1from typing import Literal 2 3from pydantic import BaseModel, ConfigDict 4 5class RunStatusUpdate(BaseModel): 6 model_config = ConfigDict(extra="forbid") 7 8 run_id: str 9 status: Literal["queued", "passed", "failed"] 10 11project_runs = {"project-alpha": {"RUN-842"}} 12 13def can_show_update(project_id: str, update: RunStatusUpdate) -> bool: 14 return update.run_id in project_runs.get(project_id, set()) 15 16parsed = RunStatusUpdate.model_validate({"run_id": "RUN-842", "status": "failed"}) 17print("format gate:", parsed.model_dump()) 18print("project allowed:", can_show_update("project-alpha", parsed)) 19print("other project allowed:", can_show_update("project-beta", parsed))
Output
1format gate: {'run_id': 'RUN-842', 'status': 'failed'} 2project allowed: True 3other project allowed: False

Production patterns

Moving from a prototype that occasionally outputs valid JSON to a production system that processes thousands of requests reliably requires defensive engineering. These patterns address the most common failure modes and lifecycle challenges associated with structured output generation.

1. Preserve useful intermediate evidence for hard tasks

A subtle but important pattern: strict structured output can hurt task quality on some hard problems when the schema is too tight. A controlled study found measurable reasoning-accuracy reductions under format restrictions on evaluated tasks such as GSM8K, with outcomes affected by format and field ordering.[9]Reference 9Let Me Speak Freely? A Study on the Impact of Format Restrictions on Performance of Large Language Modelshttps://arxiv.org/abs/2408.02442

Don't respond by storing unrestricted chain-of-thought. Instead, design product-visible intermediate fields that can be checked: cited evidence, extracted quantities, calculation steps needed by a tutor, or a reason code used by a reviewer. If a workflow genuinely needs those fields, place them before the final decision and validate them independently.

1-validate-visible-evidence-before-decision.py
1from typing import Literal 2 3from pydantic import BaseModel, model_validator 4 5class IncidentDecision(BaseModel): 6 evidence_quote: str 7 run_id: str 8 decision: Literal["retry_tests", "rollback", "escalate", "unknown"] 9 10 @model_validator(mode="after") 11 def cited_run_appears_in_evidence(self) -> "IncidentDecision": 12 if self.run_id not in self.evidence_quote: 13 raise ValueError("run ID is not supported by evidence") 14 return self 15 16decision = IncidentDecision.model_validate( 17 { 18 "evidence_quote": "Run RUN-842 failed in unit-tests after auth-cache timed out.", 19 "run_id": "RUN-842", 20 "decision": "retry_tests", 21 } 22) 23print("decision:", decision.decision) 24print("evidence checked:", decision.run_id in decision.evidence_quote)
Output
1decision: retry_tests 2evidence checked: True

This isn't about recovering a hidden "true" chain of thought. The contract exposes evidence that a product or reviewer can inspect when the final decision is wrong.

Why are visible evidence fields not the same as asking for hidden chain of thought?

Answer

You are asking for product-visible fields that your application can check: evidence quote, calculation output, reason code, or final decision. The model's private reasoning is not part of the contract.

2. Explicit intermediate fields for complex extraction

A common pitfall is forcing a difficult decision into a final field with no checkable support. For extraction, tutoring, or multi-step workflows, explicit evidence or bounded calculation fields can make the result easier to validate before code acts on it.

The schema below follows the same pattern as OpenAI's math-tutor examples: return a bounded list of explicit steps plus the final answer.

2-explicit-intermediate-fields-for-complex.py
1from pydantic import BaseModel 2 3class Step(BaseModel): 4 explanation: str 5 output: str 6 7class MathSolution(BaseModel): 8 steps: list[Step] 9 final_answer: str 10 11# Use this when the application needs the intermediate fields. 12# Keep them intentional and bounded rather than dumping unstructured text. 13

When should you add intermediate fields to a schema?

Answer

Add them when they make the task easier to validate or debug, such as extracted quote, evidence span, confidence reason, or calculation step. Don't add them as generic verbose text that nobody checks.

3. Handling schema evolution

Schemas change. If producers and consumers disagree about a payload version, a rollout can break downstream processing even when each individual response is valid JSON.

Production tip: Version your schemas in application code. Choose the validator before sending the request, then attach the corresponding version to the validated record or require a fixed version literal and verify it. Don't let the model choose which contract it claims to satisfy.

Why should a schema version travel with the validated record?

Answer

They let the backend route old and new records through the correct validator during rollouts. The application should assign or verify the version, because a model-generated version label isn't proof that the payload matches that contract.

3-route-versioned-records-with-code-owned-contracts.py
1from typing import Literal 2 3from pydantic import BaseModel 4 5class RunStatusV1(BaseModel): 6 run_id: str 7 status: str 8 9class RunStatusV2(BaseModel): 10 run_id: str 11 status: str 12 failing_job: str 13 14class VersionedRunStatus(BaseModel): 15 schema_version: Literal["2"] 16 payload: RunStatusV2 17 18generated_payload = {"run_id": "RUN-842", "status": "failed", "failing_job": "unit-tests"} 19validated = RunStatusV2.model_validate(generated_payload) 20wire_record = VersionedRunStatus(schema_version="2", payload=validated) 21print("version:", wire_record.schema_version) 22print("failing job:", wire_record.payload.failing_job)
Output
1version: 2 2failing job: unit-tests

4. Recover by failure class, without weakening the contract

Even with structured outputs, failures can happen. The important distinction is between recoverable failures (for example, truncation from max_output_tokens) and policy outcomes such as refusals or content filtering. Don't treat a refusal as an ordinary parse failure and then retry with a looser mode.[5]Reference 5Structured outputshttps://developers.openai.com/api/docs/guides/structured-outputs

Truncation doesn't justify switching from a schema-enforced response to JSON mode: the missing content is still missing and the fallback loses schema enforcement. Prefer a bounded retry with more output budget, a smaller contract, or chunked input. The fake client below mirrors response states so you can unit-test that control flow without an API call:

4-bounded-retry-preserves-schema-contract.py
1from dataclasses import dataclass 2 3from pydantic import BaseModel 4 5class RunStatusUpdate(BaseModel): 6 run_id: str 7 status: str 8 failing_job: str 9 10@dataclass 11class ContentItem: 12 type: str 13 refusal: str | None = None 14 15@dataclass 16class MessageOutput: 17 content: list[ContentItem] 18 19@dataclass 20class IncompleteDetails: 21 reason: str 22 23@dataclass 24class FakeResponse: 25 status: str 26 output: list[MessageOutput] 27 output_parsed: RunStatusUpdate | None = None 28 incomplete_details: IncompleteDetails | None = None 29 30class FakeResponsesApi: 31 def __init__(self, responses: list[FakeResponse]) -> None: 32 self.responses = iter(responses) 33 34 def parse(self, **kwargs) -> FakeResponse: 35 return next(self.responses) 36 37class FakeClient: 38 def __init__(self, responses: list[FakeResponse]) -> None: 39 self.responses = FakeResponsesApi(responses) 40 41def generate_with_bounded_retry(client: FakeClient, input_text: str) -> RunStatusUpdate: 42 response = client.responses.parse( 43 model="gpt-4o-mini", 44 input=input_text, 45 text_format=RunStatusUpdate, 46 max_output_tokens=120, 47 ) 48 49 first_content = response.output[0].content[0] 50 51 if first_content.type == "refusal": 52 raise RuntimeError(f"policy refusal: {first_content.refusal}") 53 54 if response.status == "completed" and response.output_parsed is not None: 55 return response.output_parsed 56 57 if response.status != "incomplete": 58 raise RuntimeError(f"Unexpected response status: {response.status}") 59 60 if response.incomplete_details is None: 61 raise RuntimeError("Incomplete response did not include a reason") 62 63 if response.incomplete_details.reason != "max_output_tokens": 64 raise RuntimeError( 65 f"Structured output halted: {response.incomplete_details.reason}" 66 ) 67 68 retry = client.responses.parse( 69 model="gpt-4o-mini", 70 input=input_text, 71 text_format=RunStatusUpdate, 72 max_output_tokens=400, 73 ) 74 if retry.status != "completed" or retry.output_parsed is None: 75 raise RuntimeError("bounded retry did not produce a structured result") 76 return retry.output_parsed 77 78output_item = MessageOutput(content=[ContentItem(type="output_text")]) 79incomplete = FakeResponse( 80 status="incomplete", 81 output=[output_item], 82 incomplete_details=IncompleteDetails(reason="max_output_tokens"), 83) 84completed = FakeResponse( 85 status="completed", 86 output=[output_item], 87 output_parsed=RunStatusUpdate(run_id="RUN-842", status="failed", failing_job="unit-tests"), 88) 89update = generate_with_bounded_retry( 90 FakeClient([incomplete, completed]), 91 "Run RUN-842 failed in unit-tests.", 92) 93print("retry preserved contract:", update.model_dump()) 94 95refused = FakeResponse( 96 status="completed", 97 output=[MessageOutput(content=[ContentItem(type="refusal", refusal="blocked")])], 98) 99try: 100 generate_with_bounded_retry(FakeClient([refused]), "disallowed request") 101except RuntimeError as exc: 102 print("refusal routed:", str(exc))
Output
1retry preserved contract: {'run_id': 'RUN-842', 'status': 'failed', 'failing_job': 'unit-tests'} 2refusal routed: policy refusal: blocked
A strict structured-output request splits into two paths: truncation follows a bounded retry path that keeps schema enforcement, while refusal stops and routes to policy handling without falling back to weaker raw-text parsing.
Recovery should preserve the contract. Truncation can retry with the same schema, but refusals and content-filter stops route to policy handling instead of weaker raw-text parsing.

Which structured-output failures are retryable?

Answer

Recoverable transport or length failures can use a bounded schema-preserving retry, such as increasing max_output_tokens after truncation or chunking the task. Refusals and content-filter stops are policy outcomes, so they should route to policy handling, not a looser parser.

5. Flatten nested structures

Schemas with many nesting levels or recursive shapes can increase grammar state, output length, and debugging complexity. Some hosted APIs support recursive schemas, but support doesn't establish acceptable latency for your workload.[5]Reference 5Structured outputshttps://developers.openai.com/api/docs/guides/structured-outputs Benchmark nested contracts on the target runtime.

Production tip: Keep nesting as shallow as your interface allows, bound recursive outputs, and benchmark the exact schema on your target runtime. If you need tree-shaped data, a flat list of nodes with parent_id references is often easier to generate, validate, and evolve than a recursive JSON object.

Why is a flat list with parent_id often better than recursive JSON?

Answer

It reduces grammar state, output length, validation complexity, and schema-evolution risk. You can still reconstruct the tree in code after parsing the flatter contract.

5-reconstruct-a-flat-tree-after-validation.py
1from pydantic import BaseModel 2 3class FlatNode(BaseModel): 4 node_id: str 5 parent_id: str | None 6 label: str 7 8nodes = [ 9 FlatNode(node_id="root", parent_id=None, label="ci_run"), 10 FlatNode(node_id="n1", parent_id="root", label="failing_job"), 11 FlatNode(node_id="n2", parent_id="root", label="log_url"), 12] 13children: dict[str | None, list[str]] = {} 14for node in nodes: 15 children.setdefault(node.parent_id, []).append(node.label) 16 17print("root nodes:", children[None]) 18print("run fields:", children["root"])
Output
1root nodes: ['ci_run'] 2run fields: ['failing_job', 'log_url']

Performance considerations

Grammar-guided decoding performs valid-token work during generation, although optimized engines may hide or greatly reduce observable overhead for particular workloads. Schema compliance doesn't make latency irrelevant. Measure on the model, schema, batch shape, and runtime you plan to ship.

Where the latency comes from

The latency impact depends on the schema, tokenizer, and runtime design. The main cost centers are:

Cost sourceWhy it appearsCommon mitigation
Grammar compilationThe runtime has to convert a schema or grammar into an indexable guideCompile once and reuse it across requests
Per-token maskingEach generation step must compute the valid next-token setPrecompute token-prefix tables, compressed FSMs, or categorize context-independent tokens (XGrammar)
First use of a hosted schemaSome providers or model paths may preprocess and cache a new schema before generation startsReuse stable schemas and warm hot paths when your provider documents or measurements justify it
Large prompt prefixesLong schema instructions still consume prefill work and contextUse server-side structured outputs or prefix caching

Interviewers often ask about TTFT versus TPOT for this reason. Compilation, hosted schema preprocessing, and large prompt prefixes mostly affect TTFT. Token masking affects TPOT. Systems such as Outlines, SGLang, and XGrammar focus on reducing those costs with precomputation, token categorization, and cache reuse rather than ignoring the cost.[1]Reference 1Efficient Guided Generation for Large Language Models.https://arxiv.org/abs/2307.09702[3]Reference 3SGLang: Efficient Execution of Structured Language Model Programs.https://arxiv.org/abs/2312.07104[8]Reference 8XGrammar: Flexible and Efficient Structured Generation Engine for Large Language Modelshttps://arxiv.org/abs/2411.15100[5]Reference 5Structured outputshttps://developers.openai.com/api/docs/guides/structured-outputs

Which constrained-generation costs affect TTFT versus TPOT?

Answer

Schema compilation, provider preprocessing, and long prompt prefixes usually affect Time To First Token. Per-token valid-token masking affects Time Per Output Token. Measure both because one chart can hide the bottleneck.

The quality-compliance tradeoff

Constraining the output space can affect generation quality because a grammar eliminates paths outside the contract. The effect depends on task, model, and schema; format-restriction studies show it can be measurable on reasoning tasks.[9]Reference 9Let Me Speak Freely? A Study on the Impact of Format Restrictions on Performance of Large Language Modelshttps://arxiv.org/abs/2408.02442

Production tip: Keep your schemas semantically permissive, but structurally stable. Prefer a stable field set with nullable values or bounded enums over a maze of branching object variants. If your provider uses strict mode, supported-schema limits and closed-object requirements become part of the interface contract.[5]Reference 5Structured outputshttps://developers.openai.com/api/docs/guides/structured-outputs

What is the practical schema-design tradeoff?

Answer

Make structure strict enough for code to trust, but leave values flexible enough for the model to express real cases. Overly tight enums and deep branching schemas can produce brittle or meaningless outputs.

Batched constrained generation

When processing multiple requests with the same schema (common in data extraction pipelines), the FSM can be compiled once and reused across requests. This avoids repeated per-request compilation work when the runtime supports that reuse.

Current Outlines calls support the same batching pattern. Build the model wrapper and schema-bound generator once at startup, then reuse that generator across prompts:

batched-constrained-generation.py
1# Step 1: Build the Outlines model once at startup 2# model = outlines.from_transformers(...) 3# generator = outlines.Generator(model, ExtractedEntity) 4 5# Step 2: Reuse the schema-bound generator for each prompt 6results = [ 7 ExtractedEntity.model_validate_json( 8 generator(prompt, max_new_tokens=120) 9 ) 10 for prompt in batch_prompts 11]

SGLang takes this further with prefix-aware KV cache reuse. If multiple requests share the same prompt prefix, the runtime can reuse prefetched state instead of rebuilding it from scratch.[3]Reference 3SGLang: Efficient Execution of Structured Language Model Programs.https://arxiv.org/abs/2312.07104

When does batching or cache reuse pay off for structured generation?

Answer

It pays off when many requests share the same model, schema, prompt prefix, or target type. One-off schemas get less benefit because compilation and prefill work can't be amortized across traffic.

Debugging common failures

Even with structured outputs, things go wrong. The difference between a prototype and a production system is knowing what failure looks like, why it happens, and how to fix it. This table turns common misconceptions into a debugging guide.

"The model returned valid JSON, so the data must be correct"

  • Symptom: Your pipeline parses the output successfully, but the values are nonsense. A CI status reads "passed" for a run that failed. A run ID doesn't exist in your build system.

  • Cause: Structured outputs enforce format, not accuracy. The model can produce valid JSON with correct types while the values are still fabricated. A {"status": "passed"} value satisfies the schema but is wrong for a failed run.

  • Fix: Add application-layer semantic validation. Check that run IDs exist in CI, that status values match source-of-truth logs, and that enum values match your known set. Use Pydantic validators or plain Python assertions after parsing.

Never put authorization decisions in model-owned schema fields

Wrong CI facts are one class of failure. A higher-risk class is schema-valid false authority: boolean or enum fields that look like grants.

Examples a constrained decoder will happily emit if they appear in the schema:

  • authorized: true
  • requires_approval: false
  • risk_tier: "low"
  • is_admin: true
  • approved: true

Those values are still model claims. They are not session identity, not a stored approval row, and not policy. Put only descriptive fields in the model contract (what happened, what the model proposes). Resolve grants from trusted storage: session actor, RBAC, approval records with action hashes, environment flags your host owns.

ignore-self-attested-approval.py
1from dataclasses import dataclass 2 3@dataclass(frozen=True) 4class ModelDeployProposal: 5 service: str 6 environment: str 7 approved: bool # never treat as a grant 8 9def authorize(proposal: ModelDeployProposal, stored_approval: bool) -> str: 10 if proposal.environment == "prod" and not stored_approval: 11 return "deny: missing trusted approval" 12 return "execute" 13 14proposal = ModelDeployProposal("payment-api", "prod", approved=True) 15print(authorize(proposal, stored_approval=False)) 16print("model approved field was", proposal.approved, "and was ignored")
Output
1deny: missing trusted approval 2model approved field was True and was ignored

What is the difference between format validation and semantic validation?

Answer

Format validation checks shape: JSON parses, fields exist, types match, and enums are legal. Semantic validation checks truth and operational rules: the run exists, the status matches CI, the user can access the repo, and the action is allowed. Self-attested flags such as authorized: true are still format-only; grants come from trusted host state.

"The Markdown wrapper trap"

  • Symptom: Your JSON parser throws a JSONDecodeError even though the output looks like JSON at a glance.

  • Cause: The model wrapped the JSON in triple backticks with a json label, or added a preamble like "Here is the result:". When you feed the raw output into json.loads(), the extra characters break parsing.

  • Fix: Treat unparseable or schema-invalid text as a contract failure. For a legacy prompt-only integration, make a bounded retry through a stronger interface or send the item to review; don't silently slice arbitrary text between braces and trust it as the record.

the-markdown-wrapper-trap.py
1import json 2 3from pydantic import BaseModel, ValidationError 4 5class RunStatusUpdate(BaseModel): 6 run_id: str 7 8def accept_typed_record(raw: str) -> str: 9 try: 10 payload = json.loads(raw) 11 RunStatusUpdate.model_validate(payload) 12 except (json.JSONDecodeError, ValidationError): 13 return "reject: contract not satisfied" 14 return "accept: typed record" 15 16print(accept_typed_record('{"run_id": "RUN-842"}')) 17print(accept_typed_record('Here is the JSON: {"run_id": "RUN-842"}')) 18print(accept_typed_record("```json\n{\"run_id\": \"RUN-842\"}\n```"))
Output
1accept: typed record 2reject: contract not satisfied 3reject: contract not satisfied

Use structured outputs or a grammar-guided runtime when the system must produce the contract directly rather than repair free-form text.

Why should a machine-consumed pipeline reject Markdown wrappers?

Answer

Text cleanup adds ambiguous edge cases and can hide contract drift. A schema-aware API or grammar-guided runtime produces a checked record or a failure state that application code can handle explicitly.

"A refusal isn't a parse error"

  • Symptom: Your fallback cascade retries a refused request with a looser mode, and the model still refuses. You've spent extra tokens and latency for no gain.

  • Cause: A refusal or content-filter stop is a policy outcome, not a decoding bug. The model (or the safety layer) has decided not to answer. Loosening the schema doesn't change that decision.

  • Fix: Surface the refusal to your application layer. Route it to a human reviewer, change the input, or return a polite error to the user. Don't treat refusals as retryable parse failures.[5]Reference 5Structured outputshttps://developers.openai.com/api/docs/guides/structured-outputs

Why should refusals not fall back to looser generation?

Answer

A refusal is a policy result, not a grammar failure. Retrying with fewer constraints can turn a safety decision into a bypass attempt and wastes latency without fixing the underlying request.

"Grammar-guided decoding is always slow"

  • Symptom: You've heard constrained decoding adds overhead and assume it's too slow for your use case.

  • Cause: Naive implementations can be slow, but optimized runtimes reduce the cost a lot. The overhead depends on tokenizer alignment, grammar complexity, and whether you get cache hits.

  • Fix: Benchmark before deciding. The right question isn't "is there overhead?" It's "where is the overhead, and can I amortize it?" If you process many requests with the same schema, compilation cost may be reused. Compare hosted schema APIs and optimized self-hosted runtimes on your latency and compliance targets; provider-managed enforcement hides implementation work but doesn't guarantee lower latency.[1]Reference 1Efficient Guided Generation for Large Language Models.https://arxiv.org/abs/2307.09702[3]Reference 3SGLang: Efficient Execution of Structured Language Model Programs.https://arxiv.org/abs/2312.07104[5]Reference 5Structured outputshttps://developers.openai.com/api/docs/guides/structured-outputs

What benchmark should you run before rejecting constrained decoding?

Answer

Measure the exact schema, model, tokenizer, batch shape, and traffic pattern you plan to use. Compare TTFT, TPOT, parse failure rate, retry count, and semantic validation failures against your baseline.

"JSON mode and structured outputs are the same thing"

  • Symptom: You enabled JSON mode and assumed the output would match your schema. It returned {"foo": "bar"} when you expected {"name": "string", "age": "integer"}.

  • Cause: JSON mode enforces valid JSON syntax on successful completion, but not schema compliance. The model might return any valid JSON object.

  • Fix: Use structured outputs or grammar-guided decoding when you need schema adherence. Use JSON mode only when you need syntactic validity and plan to validate the shape yourself.[5]Reference 5Structured outputshttps://developers.openai.com/api/docs/guides/structured-outputs

What should still happen after JSON mode?

Answer

Parse the JSON, validate the schema, reject unknown fields, check business rules, and handle truncation or refusal states. JSON mode is not the end of the pipeline.

"I should use structured outputs for every LLM call"

  • Symptom: You're wrapping every prompt in a Pydantic model, even for creative writing or open-ended Q&A.

  • Cause: Over-application of a useful technique. Structured outputs shine when the output feeds into code (APIs, databases, downstream processing). For user-facing text responses, free-form generation is often better.

  • Fix: Use structured outputs when the consumer is code. Use free-form generation when the consumer is a human. Forcing unnecessary structure wastes tokens on syntax characters and may constrain the model's expressiveness.

What is the simplest decision rule for using structured outputs?

Answer

If code consumes the answer, use a structured contract. If a person consumes the answer and you don't need machine routing, let the model write normally.

Practice: build an incident-digest parser

Here's a concrete exercise to test your understanding. Try it before looking at the solution sketch.

Task: You receive a 500-word incident digest about engineering services. Build a tool that extracts every mentioned service and classifies its operational status as healthy, degraded, outage, or unknown.

Requirements

  1. Define a Pydantic model with two fields: service (str) and status (Literal["healthy", "degraded", "outage", "unknown"]).
  2. Use structured outputs or grammar-guided decoding to enforce the schema.
  3. Handle the case where the model returns no mentions (return an empty list, not a null).
  4. Add a post-validation step that checks the service name against a known service catalog (e.g., auth-api, vector-indexer, billing-export). Flag unknown services for review.

Input example

"auth-api recovered after elevated 5xx errors. vector-indexer remains degraded. report-worker has no known impact."

Expected output shape

expected-output-shape.json
1[ 2 {"service": "auth-api", "status": "healthy"}, 3 {"service": "vector-indexer", "status": "degraded"}, 4 {"service": "report-worker", "status": "unknown"} 5]

Solution sketch

Click to expand solution sketch
solution-sketch.py
1from typing import Literal 2from pydantic import BaseModel 3 4class Mention(BaseModel): 5 service: str 6 status: Literal["healthy", "degraded", "outage", "unknown"] 7 8class IncidentDigest(BaseModel): 9 mentions: list[Mention] 10 11KNOWN_SERVICES = {"auth-api", "vector-indexer", "billing-export"} 12 13def unknown_services(digest: IncidentDigest) -> list[str]: 14 return [ 15 mention.service 16 for mention in digest.mentions 17 if mention.service not in KNOWN_SERVICES 18 ] 19 20digest = IncidentDigest.model_validate({ 21 "mentions": [ 22 {"service": "auth-api", "status": "healthy"}, 23 {"service": "vector-indexer", "status": "degraded"}, 24 {"service": "report-worker", "status": "unknown"}, 25 ] 26}) 27 28empty = IncidentDigest(mentions=[]) 29print("mentions:", len(digest.mentions)) 30print("unknown services:", unknown_services(digest)) 31print("empty mentions:", empty.mentions)
Output
1mentions: 3 2unknown services: ['report-worker'] 3empty mentions: []

Key design decisions:

  • Require a list, not a nullable or defaulted field: mentions: list[Mention] makes mentions required in generated JSON Schema and represents no matches as {"mentions": []}. That shape is directly compatible with OpenAI strict mode, which requires every field.
  • Post-validate status: The schema restricts the value to one of the three literals, but it can't prove the service status is factually correct. Add a second-pass check for high-stakes classifications.
  • Handle truncation: If the digest is long and the output hits max_output_tokens, use a bounded schema-preserving retry, chunk the input, or explicitly version a smaller contract.

Why does the incident-digest parser flag report-worker for review?

Answer

The phrase "no known impact" doesn't prove health, so the parser preserves uncertainty with status: "unknown". The schema can represent that value, but it can't know whether report-worker is in the service catalog. The known-service allowlist is a separate semantic check after structured parsing.

Where structured output leads

Schema-constrained generation can enforce structure during decoding. JSON mode enforces JSON syntax only, while prompt-only formatting remains best effort. The right enforcement tier, failure diagnosis, and fallback cascade determine whether truncation, refusals, and malformed output become recoverable errors or wasted tokens.

Complete the lesson

Mastery Check

Answer every question, then check your score. Score 75% or higher to mark this lesson complete.

1.A CI assistant has three LLM paths: a log extractor writes run_id and failure_type to a queue, a friendly explainer is read directly by the user, and an agent may fetch live run status with get_run_status. Which interface choice matches those consumers?

Correct answer: Use structured outputs for the log extractor, free-form text for the explainer, and function calling with strict arguments for live run status.

The deciding factor is the consumer and control flow. Code-consuming extraction needs a typed contract, user-read text can stay free-form, and live run lookup is an action request, so it belongs in a tool-call loop with validated arguments.

2.A legacy extractor asks only "Return JSON." The model replies with a preamble, a Markdown code fence, and a single-quoted object like {'run_id': 'RUN-842'}. Raw json.loads() fails. What should a production pipeline do?

Correct answer: Reject it as a contract failure; retry with schema enforcement or route it to review without trusting cleanup.

A natural-language request is not a parser-enforced contract. Preambles, code fences, and single quotes are raw-text failures for a machine interface; cleanup logic can hide contract drift and ambiguous edge cases.

3.A grammar-guided decoder has already generated {"age": for a schema requiring a non-negative integer. The candidate single-character tokens and logits are " = 5.0, a = 4.0, 7 = 0.1, and [ = 0.0. After the grammar mask, what can the sampler choose?

Correct answer: Only 7; the other candidates cannot extend a non-negative integer at that state, regardless of their original logits.

Grammar-guided decoding changes the sampling distribution before selection. Invalid next tokens are assigned negative infinity, so softmax gives them probability zero even if the model originally preferred them.

4.You are building two constrained generators: one for flat run IDs such as RUN-842, and one for arbitrary nested JSON arrays of objects. The runtime samples subword tokens, not characters. Which design implication is correct?

Correct answer: Use regex/DFA for the flat run ID, a stack-aware grammar for nested JSON, and map valid prefixes to token IDs.

Flat patterns can often be represented as regular languages and compiled to DFAs. Nested JSON needs a stack-aware grammar or parser to track balanced structures, and constrained decoding must translate valid character or byte prefixes into allowed vocabulary token IDs.

5.A structured-output call returns {"run_id": "RUN-842", "status": "failed"} and the Pydantic model accepts it. Before showing the result, your backend finds that RUN-842 belongs to project-alpha, but the authenticated user can access only project-beta. What should happen next?

Correct answer: Reject or hold the result in application-layer semantic validation because ownership and authorization are not schema properties.

A schema can prove that fields exist and have allowed types or enum values. It cannot prove database state, ownership, policy, or freshness, so those checks must run after parsing and before downstream action.

6.A constrained decoder returns schema-valid JSON: {"service": "payment-api", "environment": "prod", "authorized": true}. The deploy tool is about to run. What should the host do?

Correct answer: Ignore the model-owned authorized field and resolve approval from session identity plus a trusted approval store before any side effect.

Constrained decoding enforces shape, not authority. Self-attested grant fields are false authority claims. Identity, RBAC, and approval records must come from host-owned state, not from model-emitted booleans or enums.

7.A hosted structured-output API rejects a schema that has a root-level anyOf, optional fields, and open objects. You still need a record that downstream code can trust. What should you change?

Correct answer: Reshape the schema into the provider's supported subset, require the fields it expects, and close objects with additionalProperties: false.

A provider's supported schema subset is part of the interface contract. Closing objects rejects surprise fields, and reshaping unsupported constructs preserves schema enforcement instead of falling back to best-effort formatting.

8.Two strict structured-output requests fail. Response A has status incomplete with reason max_output_tokens. Response B contains a content item of type refusal. Which recovery path preserves the contract?

Correct answer: Retry A with the same schema using a larger output budget, a smaller contract, or chunking; route B to policy handling instead of a looser parser.

Truncation is a recoverable length or transport problem, so a bounded schema-preserving retry can help. A refusal is a policy outcome, not a grammar failure, and weakening the contract turns recovery into a bypass attempt.

9.A run-triage service is rolling out a v2 payload with a new failing_job field, and reviewers need checkable support for each decision. Which design meets both rollout and review requirements?

Correct answer: Choose the v2 validator in code, attach or verify that version on the record, and include bounded evidence fields that reviewers actually check.

The application should own schema-version selection and route records through the matching validator. For hard decisions, product-visible evidence fields can be validated and reviewed; they are not the same as asking for hidden chain of thought.

10.A batch extractor reuses one schema and a long shared prompt. First requests with a new schema have high TTFT, and a recursive tree schema raises TPOT. Which mitigation preserves structured enforcement?

Correct answer: Reuse compiled schemas or generators, cache shared prefixes where supported, and benchmark a flatter contract such as nodes with parent_id.

Schema compilation and long prefill mostly affect TTFT, while per-token valid-token masking and complex parser state can affect TPOT. Reuse and prefix caching address setup and prefill costs, and flatter contracts can reduce grammar state without abandoning enforcement.

10 questions remaining.

Next Step
Continue to ReAct & Plan-and-Execute

There, you'll compare the two core agent control loops: <span data-glossary="react">ReAct</span> for tightly coupled tool use, and Plan-and-Execute for longer workflows with explicit planning and replanning. The structured outputs you learned here become the data contracts that feed into those agent loops.

PreviousRAG Security & Access Control
Share this article
XFacebookLinkedInBlueskyRedditHacker NewsEmail
References

Efficient Guided Generation for Large Language Models.

Willard, B. T. & Louf, R. · 2023 · arXiv preprint

https://arxiv.org/abs/2307.09702

Outlines Documentation

Outlines Developers · 2026

https://dottxt-ai.github.io/outlines/latest/

SGLang: Efficient Execution of Structured Language Model Programs.

Zheng, L., et al. · 2023

https://arxiv.org/abs/2312.07104

SGLang Structured Outputs Documentation

SGLang Project · 2026

https://docs.sglang.io/docs/advanced_features/structured_outputs

Structured outputs

OpenAI · 2024

https://developers.openai.com/api/docs/guides/structured-outputs

llama.cpp: Inference of LLaMA model in pure C/C++

Gerganov, G. · 2023

https://github.com/ggml-org/llama.cpp

llama.cpp Grammars Documentation

ggml-org · 2026

https://github.com/ggml-org/llama.cpp/blob/master/grammars/README.md

XGrammar: Flexible and Efficient Structured Generation Engine for Large Language Models

Dong, Y., Ruan, C. F., Cai, Y., et al. · 2024

https://arxiv.org/abs/2411.15100

Let Me Speak Freely? A Study on the Impact of Format Restrictions on Performance of Large Language Models

Tam, Z. R., Wu, C.-K., Tsai, Y.-L., et al. · 2024

https://arxiv.org/abs/2408.02442

Discussion

Questions and insights from fellow learners.

Discussion loads when you reach this section.