Structured Outputs & Parsing

Medium 22 min read

Why Structured Output?

Why Structured Outputs Matter

The Problem: LLMs generate free-form text, but downstream code needs structured data -- JSON objects, typed fields, validated schemas. Parsing free text is fragile and error-prone.

The Solution: Structured output features force the LLM to produce valid JSON matching a specific schema, eliminating parse errors and ensuring type safety.

Real Impact: Structured outputs reduce parsing failures from 15-20% to near zero, making agents dramatically more reliable in production.

Real-World Analogy

Think of structured outputs like filling out a form vs. writing a letter:

  • Free Text = Writing a letter -- creative but hard to process automatically
  • JSON Mode = Filling out a form -- structured but flexible
  • Schema Validation = A form with required fields and data types
  • Pydantic Model = A digital form that auto-validates entries
  • Retry on Failure = "Please re-fill this field, it was invalid"

Output Strategies

JSON Mode

Tell the API to produce valid JSON. Simple but no schema enforcement -- you get JSON but not necessarily the right shape.

Schema Enforcement

Provide a JSON Schema and the API guarantees output matches. Available in OpenAI and Anthropic APIs.

Pydantic Models

Define output structure as Python classes with type annotations. Auto-generates schema and validates output.

Retry Parsing

If output fails validation, send the error back to the LLM and ask it to fix the output. Usually succeeds on retry.

JSON Mode

Output Parsing Flow
LLM Output raw text/JSON Parse JSON.parse() Validate schema check Typed Object ready to use Retry on validation error
structured_output.py
from openai import OpenAI
from pydantic import BaseModel
from typing import List

class SearchResult(BaseModel):
    title: str
    url: str
    relevance_score: float
    summary: str

class SearchResponse(BaseModel):
    query: str
    results: List[SearchResult]
    total_found: int

client = OpenAI()
response = client.beta.chat.completions.parse(
    model="gpt-4o",
    messages=[{
        "role": "user",
        "content": "Search for Python web frameworks"
    }],
    response_format=SearchResponse
)
# response.choices[0].message.parsed is a SearchResponse object
result = response.choices[0].message.parsed
print(result.results[0].title)  # Typed access!

Schema Validation

ApproachValidationType SafetyRetry Built-in
JSON ModeValid JSON onlyNoNo
response_formatSchema-enforcedYesNo
Pydantic + parse()Full type validationYesAPI-level
Instructor libraryFull + retry logicYesYes

Output Parsers

Popular Libraries

  • Instructor: Pydantic-based structured outputs with retry logic for OpenAI/Anthropic
  • LangChain OutputParsers: PydanticOutputParser, JsonOutputParser, StructuredOutputParser
  • Outlines: Constrained generation for open-source models
  • LMQL: Query language for LLMs with type constraints

Pydantic Models

FeatureDescriptionExample
Type HintsPython type annotationsname: str, age: int
ValidatorsCustom validation logic@validator for range checks
Optional FieldsFields that may be absentnickname: Optional[str]
Nested ModelsComplex object hierarchiesaddress: AddressModel
EnumsConstrained string valuesstatus: Literal["active", "inactive"]

Quick Reference

Best PracticeDescriptionWhy
Use PydanticDefine output as typed modelsAuto schema + validation
Add descriptionsDocument each fieldHelps LLM fill correctly
Set defaultsProvide fallback valuesHandles missing fields gracefully
Retry on errorSend validation errors backSelf-correction usually works
Keep schemas simpleAvoid deeply nested structuresReduces generation errors