Pydantic Schemas Fix LLM Output Fragility

Evolve from brittle json.loads() parsers to Pydantic-validated objects using OpenAI JSON Schema modes and LangChain, enforcing types, keys, and constraints at generation time for production reliability.

Overcome Parser Debt with 4 Levels of Structured Guarantees

LLM outputs start as unreliable strings—markdown-wrapped JSON, wrong keys like 'movie_title' instead of 'title', strings for integers (e.g., "2014"), or prefixed prose—crashing json.loads() or silently corrupting database inserts expecting VARCHAR title, INTEGER year, VARCHAR genre. Naive fixes add 30+ lines of if-statements for stripping, normalizing, and casting, but fail on new edge cases like XML tags or model updates.

Advance through levels: (1) Prompt-based hoping yields no guarantees; (2) JSON mode ensures parsable JSON but wrong shapes; (3) JSON Schema mode mandates exact keys, types (string, integer), enums (e.g., "action", "comedy", "sci-fi", "drama"), and no extra properties; (4) strict=True schema enforces during decoding, preventing invalid outputs like string years. For a movie schema {"title": string, "year": integer (ge=1888, le=2030), "genre": enum}, strict mode blocks generation of non-integers, restoring contract between probabilistic LLMs and deterministic apps.

Pydantic: Single Class for Schema, Validation, and Guidance

Define structures once in Python classes to auto-generate JSON Schema, validate/coerce at boundaries, and guide LLMs via Field descriptions:

from pydantic import BaseModel, Field
from enum import Enum

class Genre(str, Enum):
    ACTION = "action"
    COMEDY = "comedy"
    SCI_FI = "sci-fi"
    DRAMA = "drama"

class MovieRecommendation(BaseModel):
    title: str = Field(description="Full movie title, without year or parentheses")
    year: int = Field(ge=1888, le=2030, description="Release year as a 4-digit number")
    genre: Genre = Field(description="Primary genre - pick exactly one")

Benefits: model_json_schema() outputs full schema with refs, bounds, enums—no manual maintenance. model_validate_json('{"year": "2010"}') coerces string to int (prints 2010 as <class 'int'>), rejects invalid genre="banana" or year=99999 with ValidationError before downstream code. model_dump_json() enables clean serialization. Descriptions like "pick exactly one" improve output quality over bare fields.

For support tickets:

class Priority(str, Enum): LOW="low"; MEDIUM="medium"; HIGH="high"; URGENT="urgent"
class SupportTicket(BaseModel):
    subject: str
    priority: Priority
    product: str
    is_billing_issue: bool
    customer_sentiment: float = Field(ge=-1.0, le=1.0)
    action_items: list[str]

Extracts email into validated object for direct DB/API use, inferring priority/sentiment without parsing logic.

Integrate Natively or via LangChain for Typed Objects

OpenAI SDK (single-provider): Pass Pydantic directly—client.beta.chat.completions.parse(..., response_format=MovieRecommendation) returns .parsed as validated object, skipping json.loads() entirely.

LangChain (chains/agents): ChatOpenAI().with_structured_output(MovieRecommendation).invoke(prompt) yields typed instance. Use include_raw=True for observability, method="json_schema", strict=True (5-15% latency hit) to enforce at generation.

Both replace text-to-dict with direct domain objects, enabling composition into pipelines.

Production Rules: Reliability Over Hype

Log all ValidationErrors as signals for schema tweaks (e.g., unclear descriptions, tight bounds). Defaults: field descriptions, enums for constraints, numeric ge/le, flat schemas, strict mode. Retry strict failures with json_object fallback. This schema-first shift turns PoC hacks into systems where LLM output matches app schemas exactly, preventing bugs at DB boundaries.

Summarized by x-ai/grok-4.1-fast via openrouter

8104 input / 2015 output tokens in 14439ms

© 2026 Edge