Jake Groszewski

Blog

OpenAI Structured Outputs for Python Developers: What It Means and How to Use It

OpenAI Structured Outputs for Python Developers: What It Means and How to Use It

OpenAI's Structured Outputs feature enforces strict JSON schema compliance on model responses, eliminating the brittle string parsing and regex hacks that have plagued LLM integrations since day one. If you've built anything with the OpenAI API in Python such as data pipelines, Django backends, CLI tools, this post is for you.

This post covers what Structured Outputs actually does, when to use it, and concrete Python examples you can drop into a real project.

What Structured Outputs Solves

Previously, getting consistent JSON from a model meant:

  • Prompting the model to "respond only in JSON"
  • Using response_format={"type": "json_object"} (which enforced valid JSON but not any particular shape)
  • Writing defensive parsing code with try/except blocks everywhere
  • Retrying when the model hallucinated extra fields or dropped required ones

Structured Outputs changes this by letting you pass a JSON Schema directly to the API. The model is then constrained at the token sampling level to produce output that matches that schema exactly. No retries, no validation loops.

Requirements

  • openai Python SDK version 1.40.0 or later
  • A model that supports it: gpt-4o-2024-08-06 or newer
  • Python 3.9+

Install or upgrade:

pip install --upgrade openai

Basic Usage with a Pydantic Model

The cleanest way to use Structured Outputs in Python is with Pydantic. The SDK accepts a Pydantic model directly and handles schema generation for you.

from openai import OpenAI
from pydantic import BaseModel

client = OpenAI()

class ArticleSummary(BaseModel):
    title: str
    key_points: list[str]
    sentiment: str

response = client.beta.chat.completions.parse(
    model="gpt-4o-2024-08-06",
    messages=[
        {"role": "system", "content": "You summarize technical articles."},
        {"role": "user", "content": "Summarize this article about Python 3.13 performance improvements..."}
    ],
    response_format=ArticleSummary,
)

summary = response.choices[0].message.parsed
print(summary.title)
print(summary.key_points)

The parsed attribute gives you a fully typed Pydantic object. No json.loads, no KeyError surprises.

Using Raw JSON Schema

If you're not using Pydantic, you can pass a raw schema dict. This is useful when the schema is dynamic or comes from a database.

response = client.chat.completions.create(
    model="gpt-4o-2024-08-06",
    messages=[
        {"role": "user", "content": "Extract product info from: Blue Widget, $14.99, in stock"}
    ],
    response_format={
        "type": "json_schema",
        "json_schema": {
            "name": "product_info",
            "strict": True,
            "schema": {
                "type": "object",
                "properties": {
                    "name": {"type": "string"},
                    "price": {"type": "number"},
                    "in_stock": {"type": "boolean"}
                },
                "required": ["name", "price", "in_stock"],
                "additionalProperties": False
            }
        }
    }
)

import json
data = json.loads(response.choices[0].message.content)
print(data)

Note the "strict": True and "additionalProperties": False both are required for full enforcement.

Practical Django Integration

If you're running a Django application that uses the OpenAI API say, to extract structured data from user-submitted text, Structured Outputs fits naturally into a service layer.

# myapp/services/extraction.py

from openai import OpenAI
from pydantic import BaseModel

client = OpenAI()

class ContactExtraction(BaseModel):
    full_name: str
    email: str
    phone: str | None

def extract_contact(raw_text: str) -> ContactExtraction:
    response = client.beta.chat.completions.parse(
        model="gpt-4o-2024-08-06",
        messages=[
            {"role": "system", "content": "Extract contact details from the text."},
            {"role": "user", "content": raw_text}
        ],
        response_format=ContactExtraction,
    )
    return response.choices[0].message.parsed

Then in your Django view:

from django.http import JsonResponse
from .services.extraction import extract_contact

def extract_view(request):
    raw = request.POST.get("text", "")
    result = extract_contact(raw)
    return JsonResponse(result.model_dump())

Clean, typed, and no defensive parsing noise in your view layer.

Limitations to Know

Structured Outputs isn't magic, there are a few constraints worth noting:

  • Not all JSON Schema features are supported. Things like anyOf with more than a few variants, recursive schemas, or certain string formats may be rejected or silently ignored.
  • strict mode requires additionalProperties: false on every nested object, not just the root. Easy to miss.
  • Latency can increase slightly on the first call with a new schema because OpenAI caches the constrained grammar. Subsequent calls with the same schema are faster.
  • Refusals still happen. If the model refuses to answer, message.parsed will be None and message.refusal will contain the refusal text. Always check:
if response.choices[0].message.refusal:
    print("Refused:", response.choices[0].message.refusal)
else:
    result = response.choices[0].message.parsed

When to Use Structured Outputs vs. Function Calling

Structured Outputs and function calling both produce structured data, and the distinction can be confusing.

Scenario Use
Extract or classify data from text Structured Outputs
Trigger app-side actions (send email, query DB) Function Calling
Mix of extraction + action Function Calling with typed parameters

For pure data extraction tasks, the majority of data pipeline and Django backend use cases, Structured Outputs is simpler and more direct.

Takeaways

  • Structured Outputs enforces JSON schema compliance at the model level, not in your application code.
  • The Pydantic integration in the Python SDK is the fastest path to typed, validated model responses.
  • Django developers can drop this into a service layer cleanly with no changes to views or models required.
  • Always handle the refusal case; don't assume parsed is never None.
  • Check schema compatibility before going to production, not every JSON Schema feature is supported in strict mode.

If you've been patching around inconsistent LLM output with retry loops and manual validation, Structured Outputs is worth integrating into your stack today.