Structured Output: Guaranteed JSON
On this page
Structured Output: Guaranteed JSON
Structured Output is one of the most powerful features of the Responses API. Instead of hoping the model returns valid JSON, you guarantee it by providing a schema. The model is then structurally constrained to follow it. This transforms a language model from a creative text generator into a reliable component of an automated processing pipeline.
The problem this solves
Asking for JSON in the prompt works most of the time, which is precisely what makes the approach dangerous. One day out of a hundred, the model prefixes its response with an introductory sentence, wraps everything in a Markdown code block, or renames a field—and your json.loads() throws an exception in production on a request you cannot reproduce.
from openai import OpenAI
client = OpenAI()
# Without structure — the model can return any format
response = client.responses.create(
model="gpt-5.6-terra",
input="Give me info about Paris in JSON."
)
print(response.output_text)
# Unpredictable result:
# Sometimes valid JSON, sometimes text containing JSON,
# sometimes an unexpected format...
The text parameter changes the nature of the guarantee. You declare a json_schema format along with the expected schema, and the output conforms to it: deserialization becomes a safe operation.
response = client.responses.create(
model="gpt-5.6-terra",
input="Give me information about Paris.",
text={
"format": {
"type": "json_schema",
"name": "city_info",
"strict": True,
"schema": {
"type": "object",
"properties": {
"name": {"type": "string"},
"country": {"type": "string"},
"population": {"type": "integer"},
"official_language": {"type": "string"},
"landmarks": {
"type": "array",
"items": {"type": "string"}
}
},
"required": ["name", "country", "population", "official_language", "landmarks"],
"additionalProperties": False
}
}
}
)
import json
data = json.loads(response.output_text)
print(f"City: {data['name']}")
print(f"Population: {data['population']:,}")
print(f"Landmarks: {', '.join(data['landmarks'])}")
# Result GUARANTEED to be valid JSON:
# City: Paris
# Population: 2,161,000
# Landmarks: Eiffel Tower, Louvre, Notre-Dame, Arc de Triomphe, Sacré-Cœur
Pydantic instead of hand-written JSON Schema
Writing schemas by hand quickly becomes tedious and prone to typos. Pydantic lets you describe them as Python classes, generate the JSON Schema with model_json_schema(), and then validate and type the response with model_validate_json(). At the end of the chain, you get a full Python object with editor autocompletion, rather than an anonymous dictionary.
from pydantic import BaseModel
from typing import Optional
import json
class Product(BaseModel):
name: str
price: float
category: str
in_stock: bool
description: Optional[str] = None
response = client.responses.create(
model="gpt-5.6-terra",
input="Describe a high-end gaming laptop.",
text={
"format": {
"type": "json_schema",
"name": "product",
"strict": True,
"schema": Product.model_json_schema()
}
}
)
# Parse and validate with Pydantic
product = Product.model_validate_json(response.output_text)
print(f"Product: {product.name}")
print(f"Price: {product.price} EUR")
print(f"In stock: {'Yes' if product.in_stock else 'No'}")
# Result:
# Product: ASUS ROG Strix G18
# Price: 2499.99 EUR
# In stock: Yes
Nothing limits you to flat structures. By composing classes, you describe nested objects and lists of objects, which you will need as soon as you create a realistic customer profile: a company contains contacts, and each contact has an address.
from pydantic import BaseModel
class Address(BaseModel):
street: str
city: str
postal_code: str
country: str
class Contact(BaseModel):
last_name: str
first_name: str
email: str
phone: str
address: Address
class Company(BaseModel):
name: str
sector: str
employees: int
contacts: list[Contact]
response = client.responses.create(
model="gpt-5.6-terra",
input="Generate a fictional company profile in the tech sector "
"with 2 contacts.",
text={
"format": {
"type": "json_schema",
"name": "company",
"strict": True,
"schema": Company.model_json_schema()
}
}
)
company = Company.model_validate_json(response.output_text)
print(f"Company: {company.name} ({company.sector})")
for c in company.contacts:
print(f" - {c.first_name} {c.last_name}: {c.email}")
Two common use cases
The first is extraction: turning written text into a usable record. A conference announcement written in natural language becomes a database row with a title, date, location, and attendee count—without regular expressions, and without breaking whenever the wording changes.
class Event(BaseModel):
title: str
date: str
location: str
attendees: int
text = """
The PyCon France 2026 conference will be held on November 15 and 16
at the Palais des Congrès in Lyon. More than 800 Python developers
are expected for this edition.
"""
response = client.responses.create(
model="gpt-5.6-terra",
input=f"Extract the information from this event:\n{text}",
text={
"format": {
"type": "json_schema",
"name": "event",
"strict": True,
"schema": Event.model_json_schema()
}
}
)
evt = Event.model_validate_json(response.output_text)
print(f"{evt.title} - {evt.date} at {evt.location} ({evt.attendees} attendees)")
# Result: PyCon France 2026 - November 15-16, 2026 at Lyon (800 attendees)
The second is classification. By declaring an enumeration, you restrict the model to a closed vocabulary: it cannot answer “rather positive” or “positive” when your code expects positive. This is the difference between a value you can use directly and a value you must normalize afterward.
from enum import Enum
class Sentiment(str, Enum):
positive = "positive"
negative = "negative"
neutral = "neutral"
class Analysis(BaseModel):
sentiment: Sentiment
confidence: float
keywords: list[str]
response = client.responses.create(
model="gpt-5.6-terra",
input="Analyze this comment: 'Great service, fast delivery!'",
text={
"format": {
"type": "json_schema",
"name": "sentiment_analysis",
"strict": True,
"schema": Analysis.model_json_schema()
}
}
)
analysis = Analysis.model_validate_json(response.output_text)
print(f"Sentiment: {analysis.sentiment.value} ({analysis.confidence:.0%})")
# Result: Sentiment: positive (95%)
What strict mode requires
The guarantee comes with a trade-off: when strict: True, the schema must adhere to a specific subset of JSON Schema. These constraints are the leading cause of 400 errors for beginners, so it is best to know them before you encounter them:
- All properties must be listed in
required additionalPropertiesmust beFalse- Supported types:
string,number,integer,boolean,array,object,null - No
oneOforanyOfwith mixed types - Use
Optional[str], which translates to{"anyOf": [{"type": "string"}, {"type": "null"}]}
The rule about required often surprises people: an optional field is not expressed by removing it from the list, but by allowing the null value, exactly what Optional[str] produces on the Pydantic side.
Key takeaways
- Structured Output guarantees that the response adheres to your JSON schema
- Use
text={"format": {"type": "json_schema", ...}}in the Responses API - Pydantic is the ideal tool for defining and validating schemas
- Enable
strict: Truefor guaranteed schema compliance - Ideal for data extraction, classification, and structured generation