AI Engineering Learning Hub

Structured Output (JSON Mode / Schema)

How to get reliable JSON from an LLM: JSON mode vs schema-enforced structured outputs, Pydantic parsing, and lead capture as an example.

What it is

Structured output means forcing the model to reply in a machine-readable format that your code can trust, instead of free text you have to parse. The two levels are JSON mode, where the model promises valid JSON but you define the shape only in the prompt, and schema-enforced output, where you pass a schema (usually a Pydantic model) and the API guarantees the reply matches it.

With schema enforcement the SDK gives you a parsed Python object. No regex, no 'the model added a sentence before the JSON', no missing fields. If the model cannot satisfy the schema the call fails loudly, which is far better than a silent wrong value.

This site uses both. The chat reply is JSON mode with an intent field and the answer text. Lead capture uses a Pydantic LeadInfo schema so that a recruiter's name, email and intent go straight into a record without any text parsing.

Why it's needed

The moment an LLM's output feeds another system, a database, a workflow, a frontend component, free text becomes a liability. A reply that is 95 percent well formatted crashes the pipeline 5 percent of the time. Structured output turns the LLM into a function with a typed return value.

It also makes the model better at the task. Defining fields with descriptions is a form of prompting: 'is_lead: true only if the visitor shared contact details or hiring intent' tells the model exactly what decision to make and leaves no room for an essay.

How it works

  • Define the shape as a Pydantic model. Field descriptions are sent to the model, so write them as instructions.
  • Use optional fields with defaults for anything the input might not contain. A required field the model cannot fill forces it to invent a value.
  • Call the parse endpoint with response_format set to your model. The SDK converts the schema, validates the reply and returns a typed object.
  • For JSON mode without a schema, set response_format to json_object and describe the keys in the system prompt. Still validate the result with Pydantic on your side.
  • Keep schemas flat and small. Deep nesting and dozens of fields lower accuracy; split into two calls if needed.
  • Add a boolean 'confidence' or 'is_applicable' field so the model has a legal way to say 'this does not apply' instead of hallucinating data.

Code example

from typing import Optional
from pydantic import BaseModel, Field
from openai import OpenAI

client = OpenAI()

class LeadInfo(BaseModel):
    name: Optional[str] = Field(None, description="Visitor's name if given")
    email: Optional[str] = Field(None, description="Email address if given")
    company: Optional[str] = Field(None, description="Company they represent")
    role_or_opportunity: Optional[str] = Field(None, description="Role or project discussed")
    is_lead: bool = Field(description="True only if contact details or concrete hiring intent were shared")

def extract_lead(message: str) -> LeadInfo:
    r = client.beta.chat.completions.parse(
        model="gpt-4o-mini",
        messages=[
            {"role": "system", "content": "Extract lead details from the visitor message."},
            {"role": "user", "content": message},
        ],
        response_format=LeadInfo,          # schema enforced by the API
    )
    return r.choices[0].message.parsed     # already a LeadInfo instance

lead = extract_lead("Hi, I'm Sara from Acme. We're hiring an AI engineer, "
                    "reach me at sara@acme.com")
print(lead.is_lead, lead.email, lead.company)
# True sara@acme.com Acme

Where I used this

Lead capture on this site uses OpenAI structured output with a Pydantic LeadInfo schema (name, email, phone, company, role, is_lead). A regex pre-filter decides whether a message is worth the extra call. The chat reply itself is forced into JSON mode so the frontend can read the intent field reliably.

Use cases

  • Lead and contact extraction from chat messages
  • Classifying support tickets into category, priority and sentiment
  • Turning invoices, resumes or forms into database rows
  • Returning an intent label alongside a chat answer so the UI can react
  • Any pipeline where the next step expects a fixed data shape

Common mistakes

  • Required fields for optional information. The model will fabricate an email rather than leave a required string empty.
  • Parsing JSON out of free text with regex and hoping. Use the schema endpoint; it exists so you never do that again.
  • Trusting JSON mode to enforce your keys. It only guarantees valid JSON, not your shape. Validate with Pydantic.
  • Huge schemas. A 40-field extraction in one call is slow and error-prone; group related fields into separate calls.
  • Running structured extraction on every message. Pre-filter cheaply (regex for an email, keyword check for hiring phrases) and only call the model when it looks worthwhile.

Interview questions

What is the biggest benefit of structured output?
Reliability. Downstream code can expect the same shape every time, so the database write, the automation and the frontend never crash on a formatting surprise. It also doubles as a sharper prompt, because field descriptions tell the model exactly what to decide.
How does schema-based structured output differ from JSON mode?
JSON mode guarantees syntactically valid JSON and nothing more; the keys and types are whatever the model decides from your prompt. Schema-enforced output constrains generation to your schema, so the keys, types and enums are guaranteed and the SDK hands back a parsed object. Use JSON mode for flexible replies and schemas for data extraction.
How do you stop the model inventing values for fields it does not know?
Make those fields optional with a null default, and add a boolean like is_lead or found that lets the model say 'nothing here'. In the field description, say explicitly 'only if stated in the message'. Then test with inputs that contain no data at all.
When would you not use structured output?
For conversational replies where tone and length matter more than shape, and for long-form generation like drafting an article. Forcing prose into a JSON string field works but can flatten the writing quality and makes streaming awkward.
How does this site use structured output for lead capture?
A cheap pre-filter checks each chat message for an email, phone number or hiring phrases. Only then does a background task call the parse endpoint with a LeadInfo schema, and if is_lead is true the record is written to a JSONL file and optionally posted to a webhook. The chat reply itself is never delayed by this.
How do you validate structured output beyond the schema?
Schema validation checks types, not truth. Add business rules on your side: the email matches a regex, the date is not in the future, the category is one you support. Log rejects and add them to your test set.
Ask the AI assistant about this topic

Get new topics by email

One short, practical AI-engineering lesson whenever a new topic is published. No spam, unsubscribe any time.

Need this built for your product?

I build production RAG pipelines, LLM automations and voice agents. Open to freelance projects and full-time roles.