cd /news/artificial-intelligence/guarantee-structured-json-from-any-l… · home topics artificial-intelligence article
[ARTICLE · art-52226] src=sourcefeed.dev ↗ pub= topic=artificial-intelligence verified=true sentiment=↑ positive

Guarantee Structured JSON from Any LLM Call with OpenAI Structured Outputs and Pydantic

OpenAI's structured outputs API, combined with Pydantic, allows developers to receive fully-typed JSON objects from LLM calls, eliminating fragile parsing and schema drift. The tutorial demonstrates extracting structured meeting data using Python, Pydantic models, and the `parse()` method on supported models like GPT-4o.

read5 min views1 publishedJul 9, 2026
Guarantee Structured JSON from Any LLM Call with OpenAI Structured Outputs and Pydantic
Image: Sourcefeed (auto-discovered)

Skip the fragile JSON parsing and schema drift. Use OpenAI's structured outputs API with Pydantic to get fully-typed objects back from every model call.

Rachel Goldstein

What you'll build #

You'll write a Python script that sends unstructured text to an OpenAI model and gets back a fully-typed Pydantic object on every call. No prompt-engineering tricks, no json.loads()

in a try/except, no silent schema drift.

Prerequisites #

  • Python 3.10 or later (the str | None

union syntax and lowercase generics likelist[str]

require it) openai >= 1.40.0

(when theparse()

helper and structured outputs support shipped)pydantic >= 2.0

  • An OpenAI API key exported as OPENAI_API_KEY

  • A supported model: gpt-4o

,gpt-4o-mini

, or anygpt-4o-*

snapshot dated 2024-08-06 or later

Structured outputs don't work on gpt-4-turbo

, gpt-3.5-turbo

, or pre-August-2024 model snapshots. Passing those will give you a BadRequestError

.

1. Install dependencies #

pip install "openai>=1.40.0" "pydantic>=2.0"

Export your API key if you haven't already:

export OPENAI_API_KEY="sk-..."

The OpenAI()

client picks it up automatically from the environment.

2. Define your schema #

Here's a realistic example: extracting structured data from meeting notes.

from pydantic import BaseModel, Field

class ActionItem(BaseModel):
    owner: str = Field(description="Full name of the person responsible")
    task: str = Field(description="Specific action to be completed")
    due_date: str | None = Field(
        default=None,
        description="Due date in ISO 8601 format if mentioned, otherwise null",
    )

class MeetingExtract(BaseModel):
    summary: str = Field(description="One-sentence summary of the meeting")
    decisions: list[str] = Field(description="Key decisions made, as a list")
    action_items: list[ActionItem]

Two things worth being explicit about. First, Field(description=...)

does real work here; the model reads those descriptions when populating fields, so be precise. Second, use str

for dates, not datetime

. OpenAI's strict schema mode maps Pydantic types to JSON Schema primitives and datetime

doesn't have a clean mapping. You'll get a BadRequestError

if you try it.

3. Call the API #

The key is client.beta.chat.completions.parse()

. Pass your Pydantic class as response_format

and the SDK generates the JSON Schema, enables strict mode, and deserializes the response into your model.

from openai import OpenAI
from pydantic import BaseModel, Field

client = OpenAI()

class ActionItem(BaseModel):
    owner: str = Field(description="Full name of the person responsible")
    task: str = Field(description="Specific action to be completed")
    due_date: str | None = Field(
        default=None,
        description="Due date in ISO 8601 format if mentioned, otherwise null",
    )

class MeetingExtract(BaseModel):
    summary: str = Field(description="One-sentence summary of the meeting")
    decisions: list[str] = Field(description="Key decisions made, as a list")
    action_items: list[ActionItem]

NOTES = """
Product sync, March 12. Attendees: Priya, Dan, Leila.
Decided to drop IE11 support and push the launch to April 1st.
Dan will update the changelog by March 15. Priya owns browser compat testing,
no deadline set. Leila to schedule a stakeholder demo next week.
"""

completion = client.beta.chat.completions.parse(
    model="gpt-4o-mini",
    messages=[
        {
            "role": "system",
            "content": "Extract structured data from the meeting notes provided.",
        },
        {"role": "user", "content": NOTES},
    ],
    response_format=MeetingExtract,
)

4. Read the result #

The model can refuse if the request triggers a content policy, and generation can be cut off if the output hits the token limit. Check both before touching .parsed

.

message = completion.choices[0].message

if message.refusal:
    raise ValueError(f"Model refused: {message.refusal}")

result = message.parsed
if result is None:
    raise ValueError(
        f"Failed to parse response. Finish reason: {completion.choices[0].finish_reason}"
    )

print(result.summary)
for item in result.action_items:
    print(f"  {item.owner}: {item.task} (due: {item.due_date})")

message.parsed

is a live MeetingExtract

instance with real types, not a dict. You get autocomplete, attribute access, and Pydantic validators for free.

Expected output, roughly:

The team agreed to drop IE11 support and move the launch to April 1st.
  Dan: Update the changelog (due: 2024-03-15)
  Priya: Browser compatibility testing (due: None)
  Leila: Schedule a stakeholder demo (due: None)

Verify it works #

Run a quick sanity check after the call:

assert isinstance(result, MeetingExtract), "Expected MeetingExtract"
assert all(isinstance(i, ActionItem) for i in result.action_items)
assert isinstance(result.decisions, list)

finish_reason = completion.choices[0].finish_reason
assert finish_reason == "stop", f"Unexpected finish_reason: {finish_reason}"

print("All checks passed.")

The finish_reason

check matters. If the model is cut off mid-generation (value is "length"

), message.parsed

will be None

even with no refusal. The result is None

guard in Step 4 catches this before you ever reach the assertions. Set max_tokens

high enough for your schema's complexity if you're hitting it.

Troubleshooting #

** openai.BadRequestError: Invalid schema** Your model uses a type that doesn't map to JSON Schema. Common causes:

datetime

, Decimal

, UUID

, bare Any

, or a dict

without typed values. Swap to str

, float

, or a concrete nested Pydantic model.** message.parsed is None with no refusal** Check

finish_reason

. If it's "length"

, increase max_tokens

. If it's "stop"

and parsed

is still None

, the SDK failed to deserialize the content; inspect message.content

directly to debug the raw JSON.** AttributeError: 'ChatCompletionMessage' has no attribute 'parsed'** You called

client.chat.completions.create()

instead of client.beta.chat.completions.parse()

. The standard create()

response type doesn't include .parsed

.** ValidationError on import or at model definition time** A Pydantic v1/v2 conflict. Run

python -c "import pydantic; print(pydantic.VERSION)"

and confirm you're on 2.x. Pin with pip install "pydantic>=2.0,<3"

if another package dragged in v1.## Next steps

Raw JSON schema via REST: If you're not in Python, useresponse_format={"type": "json_schema", "json_schema": {"name": "MeetingExtract", "strict": true, "schema": {...}}}

in any HTTP client.Streaming:client.beta.chat.completions.stream()

accepts the sameresponse_format

and fires events as the object builds, useful for progressive UI updates.Function calling vs. structured outputs: use structured outputs when you want typed data returned unconditionally. Use tool calling when the model needs to decide whether to invoke a tool at all.Schema size limits: OpenAI enforces a maximum of 5 nesting levels and 100 total properties across the entire schema. Flatten aggressively if you hit this ceiling.

Rachel Goldstein· Dev Tools Editor

Rachel has been embedded in the developer tooling ecosystem for nearly eight years, covering everything from IDE wars and package-manager drama to the quiet rise of AI-assisted coding. She has a soft spot for open-source maintainers and an unhealthy number of terminal emulators installed on a single laptop.

Discussion 0 #

No comments yet

Be the first to weigh in.

── more in #artificial-intelligence 4 stories · sorted by recency
── more on @openai 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/guarantee-structured…] indexed:0 read:5min 2026-07-09 ·