Structured Outputs
Structured outputs make an AI model return data in a predictable shape, usually JSON that follows a schema. This matters when your application, workflow, or agent must parse the response and use it safely in code.
Goal
Learn how to design model outputs that software can reliably parse, validate, store, display, or pass to the next step in an agent workflow.
This topic focuses on final model responses and data extraction. Tool input schemas are covered later in Tool Schemas.
Learning Path
This topic is designed in four parts. Read them in order.
Part 1: Understand Structured Outputs
A normal model response is written for humans. A structured output is written for software.
Plain text answer:
The customer seems unhappy. They are asking for a refund because the item arrived damaged.
Structured output:
{
"sentiment": "negative",
"intent": "refund_request",
"reason": "item_arrived_damaged",
"needs_human_review": true
}
The second response is easier for code to use because every field has a predictable name and type.
What Structured Outputs Are For
Use structured outputs when the next step is software, not only a human reader.
| Use Case | Example Output |
|---|---|
| Data extraction | Pull invoice number, total, date, and vendor from a document. |
| Classification | Classify a support ticket as billing, bug, account, or other. |
| Routing | Decide which workflow, queue, or agent should handle the request next. |
| UI rendering | Return cards, table rows, labels, or form fields in a fixed shape. |
| Agent state | Return next_action, confidence, missing_info, and stop_reason. |
| API payload preparation | Produce validated data that another service can consume. |
Do not use structured outputs just because JSON looks technical. Use them when software needs dependable fields.
Prompt-Only JSON vs JSON Mode vs Structured Outputs
These are not the same thing.
| Approach | What You Ask For | Reliability | Best Use |
|---|---|---|---|
| Prompt-only JSON | "Return JSON." | Weak. The model may add prose, omit fields, or use wrong types. | Quick experiments only. |
| JSON mode | Provider tries to return valid JSON. | Better syntax, but not always your exact schema. | Simple JSON objects where strict shape is less important. |
| Structured outputs | Provider constrains output to a schema. | Strongest option when supported. | Production parsing, extraction, classification, and agent workflows. |
Structured outputs usually rely on a schema such as JSON Schema, Pydantic, or Zod. Some providers use constrained decoding or grammar-based methods so the model is guided toward valid output.
Structured Output Flow
flowchart LR
A[User input or document] --> B[Prompt]
C[Output schema] --> D[Model request]
B --> D
D --> E[Structured model output]
E --> F[Parser]
F --> G[Validator]
G -->|Valid| H[Application uses data]
G -->|Invalid| I[Retry, repair, or fallback]
How to read this diagram: the schema is part of the model request, but your application still parses and validates the response before trusting it.
Part 2: Design a Good Output Shape
A good structured output starts with a good data shape. Do not begin by writing a long prompt. Begin by deciding what your application actually needs.
Example Task
Task:
Extract support ticket details from a customer message.
Customer message:
I was charged twice for order 10452. Please refund the duplicate charge.
Useful output:
{
"intent": "refund_request",
"order_id": "10452",
"issue_summary": "Customer reports duplicate charge.",
"urgency": "medium",
"requires_human_review": true
}
Schema First, Prompt Second
Design the fields before writing the prompt.
{
"type": "object",
"additionalProperties": false,
"properties": {
"intent": {
"type": "string",
"enum": ["refund_request", "bug_report", "account_help", "general_question", "other"],
"description": "The main reason the customer is contacting support."
},
"order_id": {
"type": ["string", "null"],
"description": "The order ID mentioned by the customer, or null if not provided."
},
"issue_summary": {
"type": "string",
"description": "One short sentence summarizing the issue."
},
"urgency": {
"type": "string",
"enum": ["low", "medium", "high"],
"description": "How urgent the issue appears from the message."
},
"requires_human_review": {
"type": "boolean",
"description": "True when the case involves money, account access, legal risk, or unclear evidence."
}
},
"required": ["intent", "order_id", "issue_summary", "urgency", "requires_human_review"]
}
This schema is useful because it defines:
- exact field names
- allowed categories
- missing-data behavior
- required fields
- no unexpected extra fields
Field Design Rules
| Rule | Good | Weak |
|---|---|---|
| Use domain names | order_id |
id |
| Prefer enums for fixed choices | "urgency": "high" |
"urgency": "very important probably" |
| Make missing data explicit | "order_id": null |
omit the field randomly |
| Keep summaries short | one sentence | long explanation inside a field |
| Avoid overloaded fields | intent, urgency, summary |
analysis containing everything |
| Block extra fields when possible | additionalProperties: false |
accept any field name |
Required vs Optional
For production workflows, prefer a stable shape. A stable shape is easier to parse than a response where fields appear and disappear.
Good pattern:
{
"order_id": null,
"missing_fields": ["order_id"]
}
Risky pattern:
{
"missing_order": true
}
The risky pattern changes the shape. The good pattern keeps the same field and makes missing information explicit.
Keep Schemas Simple
Start with simple schema features:
objectarraystringnumberintegerbooleannullenumrequiredadditionalProperties
Avoid deep nesting unless the application truly needs it. Deep schemas are harder to prompt, test, debug, and migrate.
Part 3: Write Prompts That Support the Schema
A schema defines the shape. The prompt defines the task and decision rules.
If the prompt is vague, the model can still fill the schema with poor values.
Strong Structured Output Prompt
Task:
Extract support ticket details from the customer message.
Rules:
- Use only information found in the customer message.
- Do not invent an order ID.
- If the order ID is missing, set order_id to null.
- Set requires_human_review to true for refunds, payments, account access, legal complaints, or safety concerns.
- Keep issue_summary to one sentence.
Customer message:
{customer_message}
The schema controls format. The prompt controls interpretation.
Weak vs Strong
Read this message and return JSON.
The model has no clear field rules, missing-data behavior, or evidence boundary.
Extract support ticket details.
Use only the message text.
If a value is not present, use null.
Choose intent from the schema enum only.
Set requires_human_review true for payment or refund issues.
The model knows how to fill the schema and how to avoid inventing values.
Good Examples to Include
Examples are useful when categories are easy to confuse.
| Customer Message | Expected Fields |
|---|---|
| "I forgot my password." | intent: "account_help", requires_human_review: false |
| "I was charged twice." | intent: "refund_request", requires_human_review: true |
| "The export button crashes." | intent: "bug_report", requires_human_review: false |
| "Can I change the font size?" | intent: "general_question", requires_human_review: false |
Do not add too many examples. Too many examples can waste tokens and distract the model. Add examples only where the schema choices need clarification.
Structured Outputs for Agents
Agents often need structured final responses or structured intermediate decisions.
Example agent decision output:
{
"next_action": "ask_user",
"reason": "The order ID is required before checking order status.",
"missing_information": ["order_id"],
"can_finish": false
}
Useful fields for agent workflows:
| Field | Purpose |
|---|---|
next_action |
Tells the system whether to answer, ask, retrieve, call a tool, or stop. |
reason |
Short explanation for logs or debugging. |
missing_information |
Facts needed before the next step. |
confidence |
Helps route uncertain cases to review. |
can_finish |
Prevents premature final answers. |
Keep this separate from tool calling. Structured output describes what the model returns. Tool calling describes how the model asks your application to execute a function.
Part 4: Validate and Handle Failures
Structured outputs reduce format failures, but they do not remove the need for application validation.
Your code should still check:
- valid JSON
- schema match
- allowed enum values
- required fields
- business rules
- unsafe or impossible values
- provider-specific refusal or safety response fields
Validation Flow
flowchart TD
A[Receive model response] --> B{Parse JSON}
B -->|Fails| C[Retry or fallback]
B -->|Passes| D{Schema validation}
D -->|Fails| C
D -->|Passes| E{Business rules}
E -->|Fails| F[Human review or correction flow]
E -->|Passes| G[Use structured data]
How to read this diagram: schema validation is necessary, but it is not the last check. Your product rules still decide whether the data is safe and useful.
Provider and Library Patterns
Different providers expose structured outputs differently. The idea is similar: define a schema, send it with the request, parse the result, and validate it.
| Approach | What It Gives You | Watch Out For |
|---|---|---|
| Provider structured output | Strong schema-following when the provider supports it. | Supported schema features vary by provider and model. |
| JSON mode | Valid JSON syntax in many cases. | May not enforce your exact schema. |
| Tool/function calling | Structured arguments for a tool call. | Best for calling functions, not always for final user-facing output. |
| Pydantic/Zod wrappers | Typed objects, validation, retries, cleaner app code. | Still depends on provider support and good schemas. |
| Manual parsing | Full control. | More error handling, retries, and edge cases. |
Python Validation Example
from typing import Literal
from pydantic import BaseModel, ValidationError
class TicketExtraction(BaseModel):
intent: Literal[
"refund_request",
"bug_report",
"account_help",
"general_question",
"other",
]
order_id: str | None
issue_summary: str
urgency: Literal["low", "medium", "high"]
requires_human_review: bool
def parse_ticket(raw_json: str) -> TicketExtraction:
try:
return TicketExtraction.model_validate_json(raw_json)
except ValidationError as error:
raise ValueError(f"Model output did not match schema: {error}") from error
This example does not depend on one provider. The important idea is that your application treats the model output as untrusted data until validation passes.
Common Failure Modes
- Model adds prose before or after JSON
- Field is missing or renamed
- Enum value is almost correct but not exact
- Number is returned as a string
- Missing value is invented instead of set to null
- Schema is too deep or too complex
- Output is valid JSON but wrong for the business rule
- Use provider schema mode when available
- Validate with Pydantic, Zod, or JSON Schema
- Use enums for fixed categories
- Define null behavior clearly
- Set
additionalPropertiesto false when possible - Keep schemas shallow
- Retry or route to review when validation fails
Retry and Fallback Strategy
When validation fails, do not blindly continue.
Use a simple decision path:
- If the failure is minor and safe, retry once with the validation error.
- If the model refused or safety-filtered the request, handle the refusal separately.
- If required evidence is missing, ask the user or mark the field as
null. - If the data affects money, permissions, legal risk, or account access, route to human review.
- If the schema is repeatedly failing, simplify the schema or split the task.
Production Checklist
Before using a structured output in production:
- Define the schema before writing the prompt.
- Use exact enums for categories.
- Make missing-data behavior explicit.
- Validate every response in code.
- Test realistic edge cases, not only happy paths.
- Log validation failures.
- Version schemas when they change.
- Keep provider limitations in mind.
- Do not put private user data into schema names, enum values, or descriptions.
A Practical Template
Use this template when designing a structured output task.
Task:
What should the model extract, classify, or produce?
Consumer:
Which part of the application will use this data?
Schema:
- field_name: type, meaning, allowed values, required/null behavior
Evidence rules:
- What information may the model use?
- What should it do when evidence is missing?
Validation rules:
- What schema validator will be used?
- What business rules must pass after schema validation?
Failure behavior:
- Retry?
- Ask the user?
- Route to human review?
- Return a safe fallback?
Practice
Create a structured output for a support-ticket classifier.
Input examples:
I was charged twice for order 10452.
The export button crashes every time I click it.
I forgot my password and cannot log in.
Can I change the color of the dashboard?
Your output should include:
intentsummaryurgencyrequires_human_reviewmissing_information
Then test at least ten realistic messages and record:
- whether JSON parsing passed
- whether schema validation passed
- whether the classification was correct
- whether missing data was handled correctly
- whether the output was useful to downstream code
Mini Project
Build a small extraction pipeline:
- Accept one customer message.
- Ask the model for a structured output using your schema.
- Parse the response.
- Validate it with Pydantic, Zod, or JSON Schema.
- If valid, print a normalized ticket object.
- If invalid, retry once or send the case to review.
Keep the schema small. The goal is to prove that structured outputs can move from prompt result to application data safely.
Exit Criteria
You are ready to move on when you can:
- explain structured outputs in plain English
- explain the difference between prompt-only JSON, JSON mode, and schema-based structured outputs
- design a small useful schema before writing the prompt
- use enums, nulls, required fields, and
additionalPropertiescorrectly - write prompts that support the schema instead of fighting it
- validate model outputs before using them
- handle invalid, refused, incomplete, or unsafe outputs
- explain when structured outputs are better than plain text
- explain when function calling is a better fit than final structured output