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 196 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/9
Gradients and BackpropVectors, Matrices & TensorsLinear Algebra for MLAdam, Momentum, SchedulersProbability for Machine LearningBayesian Inference from EvidenceStatistics 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/15
Linear Regression from ScratchLogistic Regression and MetricsDecision Trees, Forests, and BoostingReinforcement Learning BasicsValidation and LeakageStatistical Learning and GeneralizationConformal Prediction and UncertaintyClustering and PCAProbabilistic Generative ModelsCore Retrieval AlgorithmsDecoding AlgorithmsExperiment Design and A/B TestingCausal Inference and CounterfactualsPyTorch 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/25
Dimensionality Reduction for EmbeddingsCoT, ToT & Self-Consistency PromptingFunction Calling & Tool UseMCP & Tool Protocol StandardsContext EngineeringPrompt Injection DefenseResponsible AI GovernancePrivacy-Preserving Machine LearningData 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/17
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 TuningTraining Run OperationsReward 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/31
Inference: TTFT, TPS & KV CacheMulti-Query & Grouped-Query AttentionKV Cache & PagedAttentionPrefix Caching and Prompt CachingFlashAttention & Memory EfficiencyContinuous Batching & SchedulingScaling LLM InferenceModel Parallelism for LLM InferenceAccelerator Architecture Field GuideCorrect GPU Profiling and BenchmarkingGPU Kernel Performance Engineering LabGPU Parallel Primitives LabGPU Compilers and Kernel LanguagesGEMM From Naive CUDA to Tensor CoresLow-Precision GPU ProgrammingGPU Runtime Execution LabAI-Generated Kernel EngineeringModel 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 & AutoscalingGPU Failure OperationsDistributed Inference Data PlaneServing Benchmark EngineeringA/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 Agent System Design
🎤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 124 of 196 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 CI triage pipeline receives one short log: "Run RUN-842 failed in unit-tests after auth-cache timed out." The next component doesn't want a paragraph. It needs run_id, failure_type, and failing_job so it can look up trusted CI state.

The model replies with Extracted data: { 'run_id': 'RUN-842' }. json.loads stops at the preamble and the single quotes. Even a parseable object could omit a required field or invent a value. The real question isn't whether the model can produce JSON. It's where your system draws and checks the boundary.

A secure retrieval path decides which documents may enter the model. Structured output draws the matching output boundary: which shape may leave the model and enter application code.

You already know how decoding turns logits into one token at a time, and how function calling lets a model request an application-owned tool with structured arguments. Here, those ideas meet: structured output constrains the token choices behind a record your code can parse.

Structured output generation makes model text conform to a contract. Reliable integration still needs three distinct checks: a structural contract, explicit completion and refusal handling, and application checks against real systems. The practical range runs from best-effort JSON mode to token-level constraint enforcement with grammar-guided decoding [1]Reference 1Efficient Guided Generation for Large Language Models.https://arxiv.org/abs/2307.09702. Hosted APIs such as OpenAI Structured Outputs, libraries such as Outlines, and serving stacks such as SGLang and vLLM all sit on that idea.[2]Reference 2Structured outputshttps://developers.openai.com/api/docs/guides/structured-outputs[3]Reference 3Outlines Documentationhttps://dottxt-ai.github.io/outlines/latest/[4]Reference 4SGLang: Efficient Execution of Structured Language Model Programs.https://arxiv.org/abs/2312.07104[5]Reference 5SGLang Structured Outputs Documentationhttps://docs.sglang.io/docs/advanced_features/structured_outputs[6]Reference 6vLLM: Easy, Fast, and Cheap LLM Serving with PagedAttentionhttps://github.com/vllm-project/vllm

At object start, candidate logits are The 0.45, opening brace 0.30, Sure 0.15, and JSON 0.10. The parser mask sets every illegal token to negative infinity, so only the opening brace remains sampleable.
Token The led at 0.45, but the object-start mask assigns illegal tokens -inf. Softmax then puts mass 1.0 on `{`, so the sampler can't emit a preamble.

The figure freezes one decoding step for the RUN-842 extractor. The mask guarantees a legal grammar path. It can't prove that RUN-842 exists or that unit-tests actually failed.

Where does structured output earn its keep?

Answer

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

Why "please return JSON" isn't enough

Run the weak version first. 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"""

The request sounds clear to a person. It leaves three different failure paths open:

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 outputs requires a special hallucination story. The model followed an underspecified natural-language request. You asked for JSON, but nothing in the interface enforced a shape.

That gives us the first design move: separate asking from enforcing. The tiers below differ in what the application can safely assume after generation.

Why isn't "return JSON" 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

Two decisions sit behind every machine-consumed response: which contract does application code receive, and where does enforcement run? They aren't a strict ladder. A hosted structured-output API may enforce a JSON Schema with constrained decoding hidden inside the service, while a self-hosted runtime may expose grammar-guided decoding directly. The consumer still needs a checked contract either way.

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, vLLM, llama.cpp

The same CI log makes the distinction concrete. First choose the guarantee the consumer needs. Then ask where each tier enforces it:

Prompt-based output is a hope, not a boundary. The model might return valid JSON, or it might return a monologue. A cleanup regex, parser, schema validator, and retry loop can reduce damage, but the interface remains best effort.

JSON mode moves one check into the API. A successful completion has valid JSON syntax, but you still need to request JSON, detect incomplete output, and validate the shape. It could return {"foo": "bar"} when the consumer expects {"run_id": "string", "failure_type": "string"}. JSON mode enforces syntax, not schema.[2]Reference 2Structured outputshttps://developers.openai.com/api/docs/guides/structured-outputs

Structured-output APIs accept a JSON Schema or Pydantic model. When generation completes without refusal or truncation, the returned output matches the supported schema features, including required properties and field types.[2]Reference 2Structured outputshttps://developers.openai.com/api/docs/guides/structured-outputs

Grammar-guided decoding moves enforcement into generation. The runtime masks tokens that would leave its compiled grammar, so invalid continuations receive zero probability at each step. That controls structure, not meaning. A grammar-valid payload can still contain the wrong value.[1]Reference 1Efficient Guided Generation for Large Language Models.https://arxiv.org/abs/2307.09702

What can you assume after JSON mode, and what changes with 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.

Parsing is only the first gate. The object below is valid JSON and still fails the application contract. The small standard-library validator makes that boundary visible; a deployed service would usually express the same rules with Pydantic or generated JSON Schema.

json-syntax-versus-schema-contract.py
1import json 2 3REQUIRED = ("run_id", "failure_type") 4 5def validate_triage(payload: object) -> dict[str, str]: 6 if not isinstance(payload, dict): 7 raise TypeError("root must be an object") 8 extra = sorted(set(payload) - set(REQUIRED)) 9 missing = [key for key in REQUIRED if key not in payload] 10 if extra or missing: 11 raise ValueError(f"shape mismatch extra={extra} missing={missing}") 12 record: dict[str, str] = {} 13 for key in REQUIRED: 14 value = payload[key] 15 if not isinstance(value, str): 16 raise TypeError(f"{key} must be a string") 17 record[key] = value 18 return record 19 20syntax_valid_but_wrong_shape = json.loads('{"foo": "bar"}') 21schema_valid = json.loads('{"run_id": "RUN-842", "failure_type": "test_failure"}') 22 23try: 24 validate_triage(syntax_valid_but_wrong_shape) 25except ValueError: 26 print("JSON-only result: rejected by schema") 27 28print("structured record:", validate_triage(schema_valid))
Output
1JSON-only result: rejected by schema 2structured record: {'run_id': 'RUN-842', 'failure_type': 'test_failure'}

How grammar-guided decoding works

The contract describes acceptable completed strings. A constrained decoder keeps that promise during generation by compiling a guide, tracking parser state, finding legal continuations, masking the rest, sampling one token, and updating the state. The model still supplies logits; the sampler simply can't select a token that leaves the accepted language.[1]Reference 1Efficient Guided Generation for Large Language Models.https://arxiv.org/abs/2307.09702

Diagram showing logits + parser state, mask illegal IDs, sample token, and complete?.
logits + parser state, mask illegal IDs, sample token, and complete?.

Read the diagram from left to right. The guide is compiled once for a schema; parser state and token masking are revisited for every generated token. Only after the string is complete can application code validate the record's meaning.

What promise does grammar-guided decoding actually make?

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.

From schema to logit masking

  1. Schema compilation: For regular patterns, the runtime can compile allowed strings into a Deterministic Finite Automaton (DFA), a state machine whose next state is fixed by the current state and input symbol. Nested or recursive JSON needs a context-free grammar or another stack-aware parser representation. Each runtime supports only some schema keywords, so it must reject unsupported constraints or document how it handles them.
    • Example schema: {"type": "object", "properties": {"age": {"type": "integer", "minimum": 0}}}
    • One simplified accepted form, if minimum is supported: \{"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 or bytes: 1, 2, 3, ... 9, 0, (space).
    • Invalid next characters or 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 figure's object-start step is small enough to run by hand. The has the highest raw logit, but the grammar state says only { is legal. The toy mask below assigns -inf to every other candidate before softmax:

from-schema-to-logit-masking.py
1import math 2 3NEG_INF = float("-inf") 4 5def mask_invalid_tokens(logits: dict[str, float], valid_tokens: set[str]) -> dict[str, float]: 6 return { 7 token: score if token in valid_tokens else NEG_INF 8 for token, score in logits.items() 9 } 10 11def softmax(logits: dict[str, float]) -> dict[str, float]: 12 finite = {token: score for token, score in logits.items() if score != NEG_INF} 13 peak = max(finite.values()) 14 weights = {token: math.exp(score - peak) for token, score in finite.items()} 15 total = sum(weights.values()) 16 return {token: (weights[token] / total if token in weights else 0.0) for token in logits} 17 18next_token_logits = { 19 "The": 0.45, 20 "{": 0.30, 21 "Sure": 0.15, 22 "JSON": 0.10, 23} 24 25masked = mask_invalid_tokens(next_token_logits, valid_tokens={"{"} ) 26probs = softmax(masked) 27chosen = max(probs, key=probs.get) 28print("masked logits:", {token: masked[token] for token in masked}) 29print("softmax:", {token: round(probs[token], 3) for token in probs}) 30print("chosen token:", chosen) 31assert chosen == "{" 32assert probs["{"] == 1.0
Output
1masked logits: {'The': -inf, '{': 0.3, 'Sure': -inf, 'JSON': -inf} 2softmax: {'The': 0.0, '{': 1.0, 'Sure': 0.0, 'JSON': 0.0} 3chosen token: {

The result enforces the grammar implemented by the runtime, assuming generation completes. It doesn't turn every JSON Schema keyword into a decoder rule, and support varies by runtime. Application code still has to parse and validate the finished record, then route refusals, truncation, and semantic mistakes.[2]Reference 2Structured outputshttps://developers.openai.com/api/docs/guides/structured-outputs[7]Reference 7llama.cpp Grammars Documentationhttps://github.com/ggml-org/llama.cpp/blob/master/grammars/README.md

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 DFA story leaves out the awkward boundary between grammar and model. Grammars usually speak in characters or bytes. LLMs sample subword tokens instead. At a state such as {"age": , the runtime has to ask more than "is } valid here?" It must find which of the 100,000+ vocabulary items could extend a valid character prefix from that state.[1]Reference 1Efficient Guided Generation for Large Language Models.https://arxiv.org/abs/2307.09702

That token-to-grammar mapping is where much of the implementation cost appears. Outlines precomputes an index for guided generation, and SGLang's paper describes compressed finite state machines for faster structured decoding.[1]Reference 1Efficient Guided Generation for Large Language Models.https://arxiv.org/abs/2307.09702[4]Reference 4SGLang: Efficient Execution of Structured Language Model Programs.https://arxiv.org/abs/2312.07104 A naive implementation can make masking a noticeable part of per-token latency.

Hosted APIs hide some of this machinery, but not every cost or limitation. OpenAI's current docs call out extra processing on the first Structured Outputs request with a schema for fine-tuned models; later requests with that schema avoid the extra latency, while other models don't have that limitation. Fine-tuned models also support a smaller JSON Schema subset, so pattern or numeric bounds can fail there even when they work on the base Structured Outputs path.[2]Reference 2Structured outputshttps://developers.openai.com/api/docs/guides/structured-outputs

Why is tokenizer alignment 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

Flat patterns such as a run ID, a closed enum, or one shallow object fit regular expressions and DFAs. Nested JSON asks a different question: regular constraints can't represent arbitrarily nested structures. Arrays of objects, recursive trees, and balanced { / [ pairs need a context-free grammar (CFG) or another stack-aware parser. Libraries usually translate the schema, but nested contracts still add parser-state work. Benchmark the shape on the runtime you plan to ship.

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.

Once the mask is clear, the remaining choice is operational: which runtime should own this loop?

Libraries and serving stacks

Hosted Structured Outputs APIs are convenient when their supported schema subset fits. With a self-hosted model, you choose a library or serving stack that compiles a grammar and can reuse it across requests.

Outlines

If you own the Python inference process, Outlines [1]Reference 1Efficient Guided Generation for Large Language Models.https://arxiv.org/abs/2307.09702[3]Reference 3Outlines Documentationhttps://dottxt-ai.github.io/outlines/latest/ gives you an interface over that loop. In Outlines v1, you wrap a model, bind an output type to a reusable Generator, receive a raw string, and validate it into your application type. Keep the wrapper and generator alive across requests. The selected backend still determines how grammar compilation and caching work.

This integration boundary needs outlines, transformers, pydantic, a compatible backend, and enough compute to load the model. It isn't a no-dependency local script.

outlines.py
1from typing import Literal 2 3import outlines 4from outlines import Generator 5from pydantic import BaseModel 6from transformers import AutoModelForCausalLM, AutoTokenizer 7 8class RunTriage(BaseModel): 9 run_id: str 10 failure_type: Literal["test_failure", "build_failure", "timeout"] 11 failing_job: str 12 13model_name = "HuggingFaceTB/SmolLM2-135M-Instruct" 14model = outlines.from_transformers( 15 AutoModelForCausalLM.from_pretrained(model_name), 16 AutoTokenizer.from_pretrained(model_name), 17) 18 19triage_generator = Generator(model, RunTriage) 20raw = triage_generator( 21 "Extract the run ID, failure type, and failing job: " 22 "Run RUN-842 failed in unit-tests after auth-cache timed out.", 23 max_new_tokens=120, 24) 25triage = RunTriage.model_validate_json(raw)

The enum and required fields constrain the generated shape. RunTriage.model_validate_json(raw) remains a useful second boundary: application code receives one typed object and can catch backend mismatches. CI still has to verify that RUN-842 exists and that unit-tests truly failed.

Which state should an Outlines-style integration keep warm?

Answer

Reuse the model wrapper, tokenizer or backend connection, and generators for common output types. Confirm separately whether your chosen backend caches compiled grammar state.

llama.cpp

If you run a local or embedded model, llama.cpp exposes grammar constraints through GBNF and can convert a subset of JSON Schema into grammars for its server and CLI.[8]Reference 8llama.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 The important boundary is easy to miss: a schema constrains decoding, but it doesn't teach the model what the fields mean. Plain structured generation still needs task instructions and field semantics in the prompt.[7]Reference 7llama.cpp Grammars Documentationhttps://github.com/ggml-org/llama.cpp/blob/master/grammars/README.md

What work remains for the prompt after a grammar is attached?

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.

When the required next token is far from the model's unconstrained preference, generation can stall or spend its remaining budget on whitespace. Prompt the task and field meanings; don't attach a schema and hope the mask will supply semantics.

Serving stacks: SGLang, vLLM, and XGrammar

At serving scale, request handlers usually call a stack that already knows how to compile and reuse the grammar instead of importing Outlines for every request.

SGLang's structured-output docs expose JSON Schema, regex, and EBNF, with XGrammar as the default grammar backend. Outlines and llguidance are launch-time alternatives. The same docs still ask you to put format instructions in the prompt because the mask blocks illegal tokens without teaching the task.[5]Reference 5SGLang Structured Outputs Documentationhttps://docs.sglang.io/docs/advanced_features/structured_outputs

SGLang's paper puts two different optimizations next to each other. They solve different bottlenecks:

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

Prefix caching saves prefill work when prompts share text, so it can improve Time To First Token (TTFT) for many RUN-842 requests that reuse system instructions or a schema prefix. Grammar-state work happens during decode. Both can cut latency, but they aren't the same cache.

XGrammar takes a different route through the vocabulary. It separates context-independent tokens, whose validity can be precomputed regardless of the parser stack, from context-dependent tokens, which must be checked against the current stack. A persistent stack and overlap with GPU execution then shrink per-token mask cost.[9]Reference 9XGrammar: Flexible and Efficient Structured Generation Engine for Large Language Modelshttps://arxiv.org/abs/2411.15100

The XGrammar paper reports up to 100x lower per-token grammar latency than the methods it compared, plus near-zero extra cost in some end-to-end serving setups. vLLM is another open-source server in this family: it serves batched generation and can attach a grammar engine at decode time.[6]Reference 6vLLM: Easy, Fast, and Cheap LLM Serving with PagedAttentionhttps://github.com/vllm-project/vllm[9]Reference 9XGrammar: Flexible and Efficient Structured Generation Engine for Large Language Modelshttps://arxiv.org/abs/2411.15100 Check the backend and schema subset your service actually runs. Keyword support isn't universal.

Which latency work belongs to prefix caching, and which belongs to grammar state?

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.

Where does XGrammar save work?

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 some end-to-end serving setups.

A serving engine can enforce a schema. One orchestration question remains: is the record a terminal answer, or is it an action request?

Function calling vs. structured outputs

The same JSON Schema can appear in two very different paths. Function calling proposes an action for the host to consider; structured outputs return a terminal record for code to consume. Some APIs support strict schemas for both, but the control-flow distinction still matters.

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

Which control flow does each technique create?

Answer

Choose function calling when the application needs an action request with structured arguments, whether the model chooses the tool or the host forces one. Choose structured outputs when each response should end as a data record for code to parse.

When to use function calling

Function calling fits an agent that needs to affect or inspect the world. The application exposes allowed tools, lets the model choose one or requires a specific call, executes an accepted request, and sends the result back in a later turn. The agent can then write a user-facing response from fetched data instead of guessing.

The small example leaves tool choice automatic. A question about RUN-842 can become a structured call, while a greeting can remain ordinary text:

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 for argument reliability. The call still invokes a tool rather than returning terminal data. On OpenAI, manual strict schemas should set strict: true, list every parameter in required, and close objects with additionalProperties: false. parallel_tool_calls=false can fit an application that expects zero or one call, but that setting controls orchestration, not strictness.[2]Reference 2Structured outputshttps://developers.openai.com/api/docs/guides/structured-outputs

What does a strict tool schema leave for the host?

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

Structured outputs fit data extraction and other terminal interfaces between the model and code. Unlike function calling's multi-turn loop, the usual path is one request, one schema-shaped record, and then application validation.

Here a Pydantic model defines the run-status record and goes directly to OpenAI's current Responses API parsing helper. The SDK converts the model to JSON Schema, and output_parsed exposes a typed object when generation succeeds.[2]Reference 2Structured outputshttps://developers.openai.com/api/docs/guides/structured-outputs Running the snippet requires the OpenAI Python SDK, OPENAI_API_KEY, and an OPENAI_STRUCTURED_MODEL value for a model that currently supports Structured Outputs.

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

Hosted structured-output APIs usually implement a subset of JSON Schema rather than the full specification. OpenAI's current subset requires an object at the root, disallows root-level anyOf, requires every field, and requires additionalProperties: false on every object. An optional value is represented as a required field whose type includes null.[2]Reference 2Structured outputshttps://developers.openai.com/api/docs/guides/structured-outputs Schema design belongs to API integration, not only prompt writing.

What boundary does additionalProperties: false close?

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.

The next example keeps the two gates visible. A small validator checks shape and enum values; application code then checks whether the authenticated engineer can access the run's repository.

RUN-842 with status failed passes a schema gate, then a semantic gate. Project-alpha is allowed to see the run. Project-beta is rejected even though the JSON shape is valid, because ownership is not a schema property.
Schema-valid JSON is only the first gate. RUN-842 parses, then application code checks that the caller may see project-alpha. Project-beta is held even though the shape is legal.
separate-format-from-semantic-validation.py
1ALLOWED_STATUS = {"queued", "passed", "failed"} 2PROJECT_RUNS = {"project-alpha": {"RUN-842"}} 3 4def validate_update(payload: object) -> dict[str, str]: 5 if not isinstance(payload, dict): 6 raise TypeError("root must be an object") 7 required = ("run_id", "status") 8 extra = sorted(set(payload) - set(required)) 9 missing = [key for key in required if key not in payload] 10 if extra or missing: 11 raise ValueError(f"shape mismatch extra={extra} missing={missing}") 12 run_id = payload["run_id"] 13 status = payload["status"] 14 if not isinstance(run_id, str) or status not in ALLOWED_STATUS: 15 raise TypeError("run_id must be a string and status must be a known enum") 16 return {"run_id": run_id, "status": status} 17 18def can_show_update(project_id: str, update: dict[str, str]) -> bool: 19 return update["run_id"] in PROJECT_RUNS.get(project_id, set()) 20 21parsed = validate_update({"run_id": "RUN-842", "status": "failed"}) 22print("format gate:", parsed) 23print("project allowed:", can_show_update("project-alpha", parsed)) 24print("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

The two gates catch different failures: malformed data and untrusted authority. A deployed system still has to handle hard tasks, schema rollouts, truncation, and refusals without weakening either contract.

Production patterns

Put checkable evidence in the contract

A 2024 study found that format restrictions changed accuracy on its evaluated reasoning and classification tasks, using the model versions available then. The direction wasn't universal: stricter formats often hurt reasoning, sometimes helped classification, and field order caused a large failure on one symbolic-reasoning task.[10]Reference 10Let Me Speak Freely? A Study on the Impact of Format Restrictions on Performance of Large Language Modelshttps://arxiv.org/abs/2408.02442 Use that result to motivate task-specific evaluation, not to conclude that every schema lowers quality.

Don't answer that trade-off by storing unrestricted chain-of-thought. Add product-visible evidence a reviewer can check: a cited quote, extracted quantity, calculation step, or reason code. Put it before the final decision and validate it separately. Generic "thoughts" that nobody reads add noise, not a useful contract.

validate-visible-evidence-before-decision.py
1ALLOWED_DECISIONS = {"retry_tests", "rollback", "escalate", "unknown"} 2 3def validate_decision(payload: object) -> dict[str, str]: 4 if not isinstance(payload, dict): 5 raise TypeError("root must be an object") 6 required = ("evidence_quote", "run_id", "decision") 7 extra = sorted(set(payload) - set(required)) 8 missing = [key for key in required if key not in payload] 9 if extra or missing: 10 raise ValueError(f"shape mismatch extra={extra} missing={missing}") 11 quote = payload["evidence_quote"] 12 run_id = payload["run_id"] 13 decision = payload["decision"] 14 if not isinstance(quote, str) or not isinstance(run_id, str): 15 raise TypeError("evidence_quote and run_id must be strings") 16 if decision not in ALLOWED_DECISIONS: 17 raise ValueError("unknown decision") 18 if run_id not in quote: 19 raise ValueError("run ID does not appear in evidence") 20 return {"evidence_quote": quote, "run_id": run_id, "decision": decision} 21 22decision = validate_decision( 23 { 24 "evidence_quote": "Run RUN-842 failed in unit-tests after auth-cache timed out.", 25 "run_id": "RUN-842", 26 "decision": "retry_tests", 27 } 28) 29print("decision:", decision["decision"]) 30print("evidence checked:", decision["run_id"] in decision["evidence_quote"])
Output
1decision: retry_tests 2evidence checked: True

How do visible evidence fields differ from hidden chain of thought?

Answer

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

When does an intermediate field earn its place?

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.

Version the schema in application code

Schemas change while producers and consumers are still running. If they disagree about a payload version, a rollout can break downstream processing even when every response is valid JSON.

🎯 Production tip: Version schemas in application code. Choose the validator before sending the request, then attach that version to the validated record. Don't let the model choose which contract it claims to satisfy.

Why should the validated record carry its schema version?

Answer

It lets 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.

route-versioned-records-with-code-owned-contracts.py
1def validate_run_status_v2(payload: object) -> dict[str, str]: 2 if not isinstance(payload, dict): 3 raise TypeError("root must be an object") 4 required = ("run_id", "status", "failing_job") 5 extra = sorted(set(payload) - set(required)) 6 missing = [key for key in required if key not in payload] 7 if extra or missing: 8 raise ValueError(f"shape mismatch extra={extra} missing={missing}") 9 record = {} 10 for key in required: 11 value = payload[key] 12 if not isinstance(value, str): 13 raise TypeError(f"{key} must be a string") 14 record[key] = value 15 return record 16 17generated_payload = {"run_id": "RUN-842", "status": "failed", "failing_job": "unit-tests"} 18validated = validate_run_status_v2(generated_payload) 19wire_record = {"schema_version": "2", "payload": validated} 20print("version:", wire_record["schema_version"]) 21print("failing job:", wire_record["payload"]["failing_job"])
Output
1version: 2 2failing job: unit-tests

Recover by failure class, without weakening the contract

Structured outputs still fail. Separate recoverable failures, such as truncation from max_output_tokens, from policy outcomes such as refusals or content filtering. A refusal isn't an ordinary parse failure, so don't retry it with a looser mode.[2]Reference 2Structured outputshttps://developers.openai.com/api/docs/guides/structured-outputs

Truncation also doesn't justify switching from schema enforcement to JSON mode. Missing content remains missing, and the fallback throws away the stronger contract. Prefer a bounded retry with more output budget, a smaller schema, or chunked input. The fake client below mirrors those response states so you can test the control flow without an API call:

bounded-retry-preserves-schema-contract.py
1from dataclasses import asdict, dataclass 2 3@dataclass(frozen=True) 4class RunStatusUpdate: 5 run_id: str 6 status: str 7 failing_job: str 8 9@dataclass 10class ContentItem: 11 type: str 12 refusal: str | None = None 13 14@dataclass 15class MessageOutput: 16 content: list[ContentItem] 17 18@dataclass 19class IncompleteDetails: 20 reason: str 21 22@dataclass 23class FakeResponse: 24 status: str 25 output: list[MessageOutput] 26 output_parsed: RunStatusUpdate | None = None 27 incomplete_details: IncompleteDetails | None = None 28 29class FakeResponsesApi: 30 def __init__(self, responses: list[FakeResponse]) -> None: 31 self.responses = iter(responses) 32 33 def parse(self, **kwargs) -> FakeResponse: 34 return next(self.responses) 35 36class FakeClient: 37 def __init__(self, responses: list[FakeResponse]) -> None: 38 self.responses = FakeResponsesApi(responses) 39 40def generate_with_bounded_retry(client: FakeClient, input_text: str) -> RunStatusUpdate: 41 response = client.responses.parse( 42 model="structured-output-model", 43 input=input_text, 44 text_format=RunStatusUpdate, 45 max_output_tokens=120, 46 ) 47 48 first_content = next( 49 (content for item in response.output for content in item.content), 50 None, 51 ) 52 53 if first_content is not None and first_content.type == "refusal": 54 raise RuntimeError(f"policy refusal: {first_content.refusal}") 55 56 if response.status == "completed" and response.output_parsed is not None: 57 return response.output_parsed 58 59 if response.status != "incomplete": 60 raise RuntimeError(f"Unexpected response status: {response.status}") 61 62 if response.incomplete_details is None: 63 raise RuntimeError("Incomplete response did not include a reason") 64 65 if response.incomplete_details.reason != "max_output_tokens": 66 raise RuntimeError( 67 f"Structured output halted: {response.incomplete_details.reason}" 68 ) 69 70 retry = client.responses.parse( 71 model="structured-output-model", 72 input=input_text, 73 text_format=RunStatusUpdate, 74 max_output_tokens=400, 75 ) 76 retry_content = next( 77 (content for item in retry.output for content in item.content), 78 None, 79 ) 80 if retry_content is not None and retry_content.type == "refusal": 81 raise RuntimeError(f"policy refusal: {retry_content.refusal}") 82 if retry.status != "completed" or retry.output_parsed is None: 83 raise RuntimeError("bounded retry did not produce a structured result") 84 return retry.output_parsed 85 86output_item = MessageOutput(content=[ContentItem(type="output_text")]) 87incomplete = FakeResponse( 88 status="incomplete", 89 output=[output_item], 90 incomplete_details=IncompleteDetails(reason="max_output_tokens"), 91) 92completed = FakeResponse( 93 status="completed", 94 output=[output_item], 95 output_parsed=RunStatusUpdate(run_id="RUN-842", status="failed", failing_job="unit-tests"), 96) 97update = generate_with_bounded_retry( 98 FakeClient([incomplete, completed]), 99 "Run RUN-842 failed in unit-tests.", 100) 101print("retry preserved contract:", asdict(update)) 102 103refused = FakeResponse( 104 status="completed", 105 output=[MessageOutput(content=[ContentItem(type="refusal", refusal="blocked")])], 106) 107try: 108 generate_with_bounded_retry(FakeClient([refused]), "disallowed request") 109except RuntimeError as exc: 110 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 RunStatusUpdate request branches into completed output, incomplete output with reason max_output_tokens, or a refusal item. Truncation retries once with the same schema. Refusal routes to policy handling instead of a looser parser.
Truncation of RUN-842 gets one bounded retry with the same schema. A refusal item never falls back to JSON mode or brace-slicing repair.

Which failure path is safe to retry?

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.

Flatten nested structures

Deep nesting and recursive shapes increase parser-state and debugging complexity. Some hosted APIs support recursive schemas, but support alone says nothing about acceptable latency or output size for your workload.[2]Reference 2Structured outputshttps://developers.openai.com/api/docs/guides/structured-outputs Benchmark the exact contract on the target runtime.

🎯 Production tip: Keep nesting as shallow as the interface allows, bound recursive outputs, and benchmark the exact schema on the target runtime. For 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 can a flat list with parent_id beat recursive JSON?

Answer

It bounds recursion and simplifies validation, traversal, and schema evolution. It may produce a longer payload, so benchmark output size and latency instead of assuming it's always cheaper.

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

Constrained decoding still costs time

Grammar-guided decoding does extra valid-token work while the model generates. Optimized engines can hide much of it for particular workloads, but schema compliance doesn't make latency disappear. Measure the model, schema, batch shape, and runtime you plan to ship.

Where the latency comes from

Start by separating setup work from per-token work. Schema shape, tokenizer, and runtime design decide how much each path costs:

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

Compilation, hosted schema preprocessing, and long prompt prefixes mostly delay the first token, or TTFT. Token masking repeats during decode and affects TPOT. Outlines, SGLang, XGrammar, and hosted schema caches reduce these costs with precomputation, token categorization, and reuse. None makes the work free.[1]Reference 1Efficient Guided Generation for Large Language Models.https://arxiv.org/abs/2307.09702[4]Reference 4SGLang: Efficient Execution of Structured Language Model Programs.https://arxiv.org/abs/2312.07104[9]Reference 9XGrammar: Flexible and Efficient Structured Generation Engine for Large Language Modelshttps://arxiv.org/abs/2411.15100[2]Reference 2Structured outputshttps://developers.openai.com/api/docs/guides/structured-outputs

Which costs delay the first token, and which repeat during decode?

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 also change quality: the grammar removes paths outside the contract. The effect depends on task, model, and schema, and format-restriction studies show measurable changes on reasoning tasks.[10]Reference 10Let 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 schemas semantically permissive but structurally stable. Prefer a stable field set with nullable values or bounded enums over a maze of branching object variants. With strict mode, supported-schema limits and closed-object requirements become part of the interface contract.[2]Reference 2Structured outputshttps://developers.openai.com/api/docs/guides/structured-outputs

Where should schema strictness stop?

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.

Compile once, then reuse the guide

When requests share a schema, compile the guide once and reuse it. The toy mask table below repeats the figure's object-start rule across a batch. A serving stack does the same with a compiled grammar and can also reuse the KV cache when prompts share a prefix.[4]Reference 4SGLang: Efficient Execution of Structured Language Model Programs.https://arxiv.org/abs/2312.07104

reuse-compiled-object-start-mask.py
1LEGAL_BY_STATE = { 2 "object_start": {"{"}, 3} 4 5def sample(state: str, logits: dict[str, float]) -> str: 6 legal = LEGAL_BY_STATE[state] 7 masked = { 8 token: score if token in legal else float("-inf") 9 for token, score in logits.items() 10 } 11 return max(masked, key=masked.get) 12 13batch_logits = [ 14 {"The": 0.45, "{": 0.30, "Sure": 0.15}, 15 {"Sure": 0.50, "{": 0.20, "The": 0.10}, 16] 17print([sample("object_start", logits) for logits in batch_logits])
Output
1['{', '{']

When does reuse pay for itself?

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

Structured output narrows the failure surface, but it doesn't remove it. Debugging gets easier when you can name the observed symptom, connect it to the boundary that failed, and choose a recovery that preserves the contract. The cases below use the RUN-842 path to make those decisions concrete.

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

Your parser can succeed while the values are nonsense: a failed CI run reads "passed", or a run ID doesn't exist in the build system. Structured outputs enforce format, not accuracy. A {"status": "passed"} value satisfies the schema while contradicting the source log.

Keep the format gate, then 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. A schema library or plain assertions after parsing can do the work; the host has to own the check.

Never put authorization decisions in model-owned schema fields

Wrong CI facts are one class of failure. A more dangerous 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 remain model claims. They aren't session identity, a stored approval row, or policy. Keep the model contract descriptive: what happened and what the model proposes. Resolve grants from trusted storage such as the session actor, RBAC, approval records with action hashes, and environment flags owned by the host.

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

Which gate can catch a schema-valid false authority?

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"

The next failure is easier to reproduce: json.loads() raises JSONDecodeError even though the output looks like JSON at a glance. The model may have wrapped it in triple backticks with a json label or added "Here is the result:" before the object. Those extra characters break the raw parse.

Treat unparseable or schema-invalid text as a contract failure. A legacy prompt-only integration can make a bounded retry through a stronger interface or send the item to review. It shouldn't silently slice between braces and promote arbitrary text to a record.

the-markdown-wrapper-trap.py
1import json 2 3REQUIRED = ("run_id",) 4 5def accept_typed_record(raw: str) -> str: 6 try: 7 payload = json.loads(raw) 8 except json.JSONDecodeError: 9 return "reject: contract not satisfied" 10 if not isinstance(payload, dict): 11 return "reject: contract not satisfied" 12 extra = set(payload) - set(REQUIRED) 13 missing = [key for key in REQUIRED if key not in payload] 14 if extra or missing: 15 return "reject: contract not satisfied" 16 if not isinstance(payload["run_id"], str): 17 return "reject: contract not satisfied" 18 return "accept: typed record" 19 20print(accept_typed_record('{"run_id": "RUN-842"}')) 21print(accept_typed_record('Here is the JSON: {"run_id": "RUN-842"}')) 22print(accept_typed_record("```json\n{\"run_id\": \"RUN-842\"}\n```"))
Output
1accept: typed record 2reject: contract not satisfied 3reject: contract not satisfied

If the system must produce the contract directly, use structured outputs or a grammar-guided runtime instead of repairing free-form text.

Why should a machine-consuming 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"

Suppose a fallback cascade retries a refused request with a looser mode and receives another refusal. The extra tokens and latency haven't changed the outcome. A refusal or content-filter stop is a policy result, not a decoding bug: the model or safety layer has decided not to answer, and loosening the schema doesn't change that decision.

Surface the refusal to the application layer. Route it to a reviewer, change the input, or return a clear error to the user. Don't treat it as a retryable parse failure.[2]Reference 2Structured outputshttps://developers.openai.com/api/docs/guides/structured-outputs

Why shouldn't a refusal 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"

You may hear that constrained decoding adds overhead and conclude that it's too slow. Naive implementations can be slow, while optimized runtimes reduce the cost substantially. The overhead depends on tokenizer alignment, grammar complexity, and cache hits.

Benchmark before deciding. Ask where the overhead appears and whether traffic can amortize it. If many requests share a schema, compilation can be reused. Compare hosted schema APIs with 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[4]Reference 4SGLang: Efficient Execution of Structured Language Model Programs.https://arxiv.org/abs/2312.07104[2]Reference 2Structured outputshttps://developers.openai.com/api/docs/guides/structured-outputs

What should a constrained-decoding benchmark hold constant?

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.

"The grammar is fighting the model"

Sometimes the response has the right shape but is empty, padded with whitespace, or truncated at max_output_tokens. The mask may be forcing a token that the model assigned very little probability. Both llama.cpp and SGLang warn that a grammar doesn't teach field meaning, so the prompt still needs to describe the desired format.[7]Reference 7llama.cpp Grammars Documentationhttps://github.com/ggml-org/llama.cpp/blob/master/grammars/README.md[5]Reference 5SGLang Structured Outputs Documentationhttps://docs.sglang.io/docs/advanced_features/structured_outputs

Keep the schema and strengthen the task instructions. If generation still stalls, simplify or bound the contract, or split the job. Raw text shouldn't be the first recovery.

"JSON mode and structured outputs are the same thing"

JSON mode can return {"foo": "bar"} when you expect {"name": "string", "age": "integer"}. It enforces valid JSON syntax on successful completion, not schema compliance, so any valid JSON object may pass.

Use structured outputs or grammar-guided decoding when you need schema adherence. JSON mode fits cases where syntactic validity is enough and your application will validate the shape itself.[2]Reference 2Structured outputshttps://developers.openai.com/api/docs/guides/structured-outputs

What work remains after JSON mode?

Answer

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

"I should use structured outputs for every LLM call"

The opposite mistake is wrapping every prompt in a Pydantic model, including creative writing and open-ended Q&A. Structured outputs shine when the result feeds code such as an API, database, or downstream processor. User-facing text often benefits from free-form generation.

Match the contract to the consumer. Use structured outputs when code consumes the answer and free-form generation when a person does. Unnecessary structure spends tokens on syntax and can narrow the model's expressive range.

What simple rule picks the interface?

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

Now carry the boundary into a small incident parser. Read the requirements, sketch the contract, and predict the empty and unknown cases before opening the solution.

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. Return a mentions list. Each item has service and status.
  2. Use structured outputs or grammar-guided decoding to enforce the schema.
  3. Handle no mentions as {"mentions": []}, not null or a missing field.
  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 "mentions": [ 3 {"service": "auth-api", "status": "healthy"}, 4 {"service": "vector-indexer", "status": "degraded"}, 5 {"service": "report-worker", "status": "unknown"} 6 ] 7}

One possible implementation

Reveal one implementation
solution-sketch.py
1ALLOWED_STATUS = {"healthy", "degraded", "outage", "unknown"} 2KNOWN_SERVICES = {"auth-api", "vector-indexer", "billing-export"} 3 4def validate_digest(payload: object) -> dict[str, list[dict[str, str]]]: 5 if not isinstance(payload, dict) or set(payload) != {"mentions"}: 6 raise ValueError("root must be an object with only mentions") 7 mentions = payload["mentions"] 8 if not isinstance(mentions, list): 9 raise TypeError("mentions must be a list") 10 checked: list[dict[str, str]] = [] 11 for item in mentions: 12 if not isinstance(item, dict): 13 raise TypeError("each mention must be an object") 14 extra = sorted(set(item) - {"service", "status"}) 15 missing = [key for key in ("service", "status") if key not in item] 16 if extra or missing: 17 raise ValueError(f"mention shape extra={extra} missing={missing}") 18 service = item["service"] 19 status = item["status"] 20 if not isinstance(service, str) or status not in ALLOWED_STATUS: 21 raise TypeError("service must be a string and status must be a known enum") 22 checked.append({"service": service, "status": status}) 23 return {"mentions": checked} 24 25def unknown_services(digest: dict[str, list[dict[str, str]]]) -> list[str]: 26 return [ 27 mention["service"] 28 for mention in digest["mentions"] 29 if mention["service"] not in KNOWN_SERVICES 30 ] 31 32digest = validate_digest( 33 { 34 "mentions": [ 35 {"service": "auth-api", "status": "healthy"}, 36 {"service": "vector-indexer", "status": "degraded"}, 37 {"service": "report-worker", "status": "unknown"}, 38 ] 39 } 40) 41empty = validate_digest({"mentions": []}) 42print("mentions:", len(digest["mentions"])) 43print("unknown services:", unknown_services(digest)) 44print("empty mentions:", empty["mentions"])
Output
1mentions: 3 2unknown services: ['report-worker'] 3empty mentions: []

The implementation makes three deliberate choices:

  • Require a list, not a nullable field: {"mentions": []} is the empty case. That shape matches OpenAI strict mode, which requires every field.
  • Post-validate the catalog: the schema can restrict status to four literals, but it can't know whether report-worker is a real service.
  • Handle truncation: if the digest is long and output hits max_output_tokens, retry with the same schema, chunk the input, or 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

The incident parser leaves a useful boundary in view. Schema-constrained generation can enforce structure during decoding; JSON mode enforces syntax only; prompt-only formatting remains best effort. From there, the host owns the meaning: it validates evidence and authority, versions contracts, and routes truncation separately from refusals. The next step is to place these reliable records inside an agent loop and decide which one should trigger a tool.

Complete the lesson

Mastery Check

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

1.CI product has queue-bound log extractor, user-read explanation, and agent that may fetch live run state. Which contract and control flow fit each consumer?

Correct answer: Use structured output for extractor, free-form prose for explanation, and strict function call for host-executed live lookup.

Terminal code-consumed data needs schema; human prose needn't pay syntax tax; external lookup is action proposal followed by runtime execution and observation turn.

2.JSON mode returns parseable {"foo":"bar"} for required {run_id,failure_type} contract. What does this outcome prove and require?

Correct answer: It proves JSON syntax only; application must validate closed shape and use structured output when schema adherence is required.

JSON mode constrains syntax on successful completion, not expected keys, types, or semantics. Parseability is first gate, not interface contract.

3.Decoder state expects object start. Logits are The 0.45, { 0.30, Sure 0.15, JSON 0.10; grammar allows only {. What distribution reaches sampler?

Correct answer: { receives probability 1.0 after illegal logits become negative infinity, despite lower original score than The.

Masking happens before softmax. Only legal continuation retains finite logit, so normalized mass collapses to it. This enforces grammar path, not field truth.

4.Runtime must constrain flat RUN-842 identifiers and arbitrary nested JSON while model samples 100k subword tokens. Which implementation model is correct?

Correct answer: Use DFA or regex for flat pattern, stack-aware parser for nesting, and map valid byte prefixes to allowed token IDs per state.

Regular state handles flat language; nested balanced structures require stack. Tokenizer alignment translates character or byte legality into vocabulary mask at every decoding step.

5.Schema-valid payload says RUN-842 failed and authorized:true; database says run belongs to project-alpha while session may access only project-beta. What should host do?

Correct answer: Ignore self-attested grant, verify run and status against trusted systems, and reject cross-project access before display.

Format fields are model claims. Semantic truth, ownership, RBAC, and approval come from host-owned systems even when constrained decoder guaranteed exact shape.

6.Provider strict schema rejects root anyOf, omittable fields, and open nested objects. Which redesign preserves strongest available contract?

Correct answer: Reshape into supported object root, require all fields with nullable types where needed, and close every object.

Provider subset is interface constraint. Redesign preserves enforced schema instead of falling back to best-effort JSON or prompt-only contract.

7.Response A is incomplete due to max_output_tokens; response B contains refusal; legacy response C has Markdown-wrapped JSON. Which recovery preserves policy and contract?

Correct answer: Retry A once with same schema and more budget or smaller task; route B to policy; reject C or retry through enforced interface.

Length failure is bounded and schema-preserving retry candidate. Refusal is policy outcome. Wrapper is contract violation, not trustworthy cleanup opportunity.

8.Run-triage v2 adds failing_job, and reviewer needs checkable support. Which rollout artifact should application produce?

Correct answer: Application chooses v2 validator, attaches verified version after validation, and includes bounded evidence fields reviewers inspect.

Code owns contract version and routing. Product-visible quote, quantity, or reason code supports review without persisting unconstrained private reasoning.

9.First request for schema has high TTFT; recursive contract increases TPOT. Which diagnosis and mitigation distinguish two costs?

Correct answer: Compilation and shared prefix prefill affect TTFT; parser-state masking affects TPOT. Reuse guide/cache and benchmark flatter schema.

Setup and prefill delay first token; legal-token computation repeats during decode. Stable reuse and flatter parent-reference designs target distinct bottlenecks without dropping enforcement.

10.Model's unconstrained preference conflicts with grammar and output becomes empty or whitespace-heavy before truncation. Which repair should come first?

Correct answer: Keep constraint, explain task and field semantics clearly, then simplify or bound schema and split task if generation still stalls.

Grammar blocks illegal tokens but doesn't teach desired values. Prompt supplies semantics; contract remains enforced while complexity and output size are reduced deliberately.

10 questions remaining.

Next Step
Continue to ReAct & Plan-and-Execute

A schema gives an agent loop reliable fields to inspect, including tool arguments that your runtime can validate before any side effect. The next chapter compares <span data-glossary="react">ReAct</span> for tightly coupled tool use with Plan-and-Execute for longer workflows, where those contracts make each step checkable.

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

Structured outputs

OpenAI · 2024

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

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

vLLM: Easy, Fast, and Cheap LLM Serving with PagedAttention

vLLM Team · 2024

https://github.com/vllm-project/vllm

llama.cpp Grammars Documentation

ggml-org · 2026

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

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

Gerganov, G. · 2023

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

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.