Large Language Models (LLMs) are inherently probabilistic sequence predictors. When building production software on top of models like GPT-4o, Claude 3.7 Sonnet, or Gemini 2.5 Pro, extracting structured data (such as extracting CRM entities, categorizing tickets, or populating relational databases) historically relied on heuristic prompt engineering, few-shot examples, and brittle regex parsing.
Modern AI engineering standardizes on Structured Outputs via JSON Schema enforcement, where sampling engines constrain the decoding token vocabulary using Context-Free Grammars (CFGs) or deterministic finite-state automata (FSMs). This guarantees 100% adherence to your required schema without parsing errors.
This guide details how structured decoding works under the hood, how to construct strict JSON schemas, how to handle multi-provider differences, and how to optimize context window token usage.
1. How Constrained Decoding & Structured Outputs Work
In standard autoregressive generation, a model predicts the probability distribution $P(w_t \mid w_{<t})$ over its entire tokenizer vocabulary (typically 32k to 128k+ tokens).
When structured outputs are enforced:
- Grammar Compilation: The engine compiles your JSON Schema into a pushdown automaton (PDA) or regular grammar.
- Logit Masking: At each generation step $t$, the sampling engine sets the logits of all tokens that would violate the grammar to $-\infty$.
- Zero Parse Failures: The model cannot emit malformed keys, invalid datatypes, unescaped strings, or unexpected properties.
Prompt + JSON Schema ──▶ Compile Grammar (FSM/CFG) ──▶ Autoregressive Logit Masking ──▶ Guaranteed Valid JSON
2. Defining Strict JSON Schemas for OpenAI
OpenAI requires the response_format configuration to specify type: "json_schema" along with strict: true.
Mandatory Rules for strict: true:
additionalProperties: falsemust be explicitly set on every object schema.requiredArray: Every property declared inpropertiesmust be listed in therequiredarray. Optional fields must be modeled as nullable unions (type: ["string", "null"]).- No Disallowed Keywords: Keywords like
patternProperties,default, or complexformatvalidators outside supported subsets will trigger validation rejection.
{
"name": "extracted_invoice_payload",
"strict": true,
"schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"invoiceNumber": {
"type": "string",
"description": "Unique invoice identifier"
},
"issuedDate": {
"type": "string",
"format": "date",
"description": "ISO 8601 date string (YYYY-MM-DD)"
},
"lineItems": {
"type": "array",
"items": {
"type": "object",
"properties": {
"description": { "type": "string" },
"quantity": { "type": "integer" },
"unitPrice": { "type": "number" },
"sku": { "type": ["string", "null"] }
},
"required": ["description", "quantity", "unitPrice", "sku"],
"additionalProperties": false
}
},
"totalAmount": { "type": "number" }
},
"required": ["invoiceNumber", "issuedDate", "lineItems", "totalAmount"],
"additionalProperties": false
}
}
Tip: You can design, inspect, and generate provider-compliant strict JSON schemas instantly with the DevFlow LLM JSON Schema Generator.
3. Implementing Structured Outputs across Providers
TypeScript (Vercel AI SDK & OpenAI SDK)
import { openai } from '@ai-sdk/openai';
import { generateObject } from 'ai';
import { z } from 'zod';
const InvoiceSchema = z.object({
invoiceNumber: z.string().describe('Unique invoice identifier'),
issuedDate: z.string().describe('ISO-8601 formatted date'),
lineItems: z.array(
z.object({
description: z.string(),
quantity: z.number().int().positive(),
unitPrice: z.number().positive(),
sku: z.string().nullable(),
})
),
totalAmount: z.number(),
});
export async function parseInvoice(documentText: string) {
const { object } = await generateObject({
model: openai('gpt-4o-2024-08-06'),
schema: InvoiceSchema,
schemaName: 'InvoiceExtraction',
mode: 'json',
prompt: `Extract line-item details from this invoice: \n\n${documentText}`,
});
return object; // Fully typed as z.infer<typeof InvoiceSchema>
}
Python (Anthropic Tool Use / Structured Outputs)
Anthropic Claude utilizes tools with tool_choice: {"type": "tool", "name": "record_invoice"} to guarantee structured emission:
import anthropic
from pydantic import BaseModel, Field
from typing import List, Optional
class LineItem(BaseModel):
description: str
quantity: int
unit_price: float
sku: Optional[str] = None
class Invoice(BaseModel):
invoice_number: str
issued_date: str
line_items: List[LineItem]
total_amount: float
client = anthropic.Anthropic()
response = client.messages.create(
model="claude-3-7-sonnet-20250219",
max_tokens=2048,
tools=[{
"name": "record_invoice",
"description": "Record extracted invoice data into the ERP database.",
"input_schema": Invoice.model_json_schema()
}],
tool_choice={"type": "tool", "name": "record_invoice"},
messages=[{
"role": "user",
"content": f"Extract the invoice payload:\n{document_text}"
}]
)
tool_call = next(b for b in response.content if b.type == "tool_use")
extracted_data = Invoice(**tool_call.input)
4. Token & Latency Optimization Strategies
Structured outputs introduce trade-offs in grammar compilation time and prompt token volume:
- First-Token Latency (Time to First Token - TTFT): On OpenAI, the first API request with a new schema undergoes one-time compilation overhead (typically 200–800ms). Subsequent requests reusing the exact identical schema name and structure hit the compiled grammar cache.
- Prompt Bloat: Deeply nested JSON schemas with verbose property descriptions can consume thousands of context tokens. Use DevFlow AI Token Counter to calculate exact token overhead before sending schemas across batch pipelines.
- Enum vs Free-Form String: Whenever a property has a bounded set of valid values, enforce
enum: ["PAID", "PENDING", "REFUNDED"]. This restricts the logit candidate space, improving token generation speed and eliminating hallucinated statuses.
5. Structured Outputs vs Function Calling vs JSON Mode
| Dimension | JSON Mode (type: "json_object") |
Function Calling / Tool Use | Structured Outputs (strict: true) |
|---|---|---|---|
| Syntax Guarantee | Guaranteed valid JSON syntax | High probability of valid JSON | 100% Guaranteed mathematical adherence |
| Schema Adherence | ❌ None (hallucinates schema keys) | ⚠️ Moderate (occasional missed fields) | ✅ 100% Strict (all fields + constraints enforced) |
| Parsing Effort | Requires manual JSON.parse + validation |
SDK helper extraction | Direct zero-error parsing |
| Compilation Latency | None | Low | Initial compilation on new schema hash |
Frequently Asked Questions
Can an LLM return a 400 error if the input does not match the schema?
No. The model will force its generated tokens into the schema format regardless of the input document content. If you want the model to indicate that it could not find relevant data, provide an explicit fallback field in your schema (e.g., "extractionSuccess": { "type": "boolean" } or "errorReason": { "type": ["string", "null"] }).
Does strict: true guarantee the factual accuracy of the data?
No. Structured outputs constrain the shape and datatype of the generated output, not its factual veracity. Grounding must still be enforced via clean context retrieval, system prompt guardrails, and RAG pipelines.
How do I convert an existing JSON payload sample into an LLM-ready schema?
You can use the DevFlow JSON to Schema Converter to infer initial types, and then use the LLM JSON Schema Generator to convert it into strict provider formats.