Skip to main content
Lesson 9 of 22 · Responses API: OpenAI Fundamentals

Structured Output: Guaranteed JSON

Lesson 9 of 22 Updated July 29, 2026 Developer
On this page
  1. Structured Output: Guaranteed JSON
  2. The problem this solves
  3. Pydantic instead of hand-written JSON Schema
  4. Two common use cases
  5. What strict mode requires
  6. Key takeaways

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
  • additionalProperties must be False
  • Supported types: string, number, integer, boolean, array, object, null
  • No oneOf or anyOf with 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: True for guaranteed schema compliance
  • Ideal for data extraction, classification, and structured generation