← chapter

Structured output and JSON mode

Chapter 5 · stop parsing prose, start declaring schemas

The hour

The problem

The model hands you a string.

That's fine for a human. It's a bug the moment code reads it — the day it writes "Sure! Here's the JSON:" in front of the answer.

The schema is the contract

class CalendarEvent(BaseModel):
    name: str
    date: str
    participants: list[str]

Declare the shape once. Both providers enforce it.

OpenAI: responses.parse

response = client.responses.parse(
    model="gpt-5.4-nano",
    input=f"Extract the event:\n{TEXT}",
    text_format=CalendarEvent,
)
event = response.output_parsed   # a real CalendarEvent

Enforced at decode time, not suggested in the prompt.

Gemini: response_schema

response = client.models.generate_content(
    model="gemini-3.1-flash-lite",
    contents=f"Extract the event:\n{TEXT}",
    config=types.GenerateContentConfig(
        response_mime_type="application/json",
        response_schema=CalendarEvent,
    ),
)
event = response.parsed

Same model, two SDKs

One Pydantic model. text_format in OpenAI, response_schema in Gemini. The shape stops being a per-provider concern.

Put it to work — three apps

Each one is a docker compose up web app.

Takeaway

When output feeds code, declare a schema. It turns "usually parseable" into "validated or it failed". "Usually" is the word that pages you at 2am.