From loose dict to validated model
Python's json.loads gives you a dict — untyped, unvalidated, and happy to hand you a KeyError at runtime three functions later. Pydantic turns that JSON into a *typed, validated model*: you declare the shape once as a class, and Pydantic parses, coerces, and validates incoming JSON against it, raising a precise error the moment data doesn't fit. It's the validation layer behind FastAPI and, increasingly, the standard way Python apps get structured output from LLMs. If you've used the JSON to Python converter for plain types, Pydantic is the next step up: types *plus* runtime validation.
The basic mapping
Given a JSON object, each key becomes a typed field on a BaseModel subclass. This JSON:
{ "id": 1, "name": "Ada", "active": true, "score": 9.5 }maps to:
from pydantic import BaseModel
class User(BaseModel):
id: int
name: str
active: bool
score: float
user = User.model_validate_json('{"id": 1, "name": "Ada", "active": true, "score": 9.5}')
print(user.name) # "Ada" — typed attribute access, not user["name"]model_validate_json parses and validates in one step. Pass data that doesn't match — a string where an int belongs, a missing required field — and Pydantic raises a ValidationError listing exactly which fields failed and why, instead of failing silently later.
Optional and defaulted fields
A key that may be absent or null maps to Optional (or a default). This is the most common source of JSON-to-model bugs, so be deliberate:
from pydantic import BaseModel
class User(BaseModel):
id: int
name: str
nickname: str | None = None # may be missing or null
role: str = "member" # defaulted if absentA field with no default is required; give it a default (or None) to make it optional. Matching this to your real payloads — which fields are guaranteed, which aren't — is what makes the model trustworthy.
Nested objects become nested models
Nested JSON maps to nested models, and Pydantic validates the whole tree recursively:
{
"id": 1,
"name": "Ada",
"address": { "city": "London", "zip": "EC1" },
"tags": ["admin", "beta"]
}from pydantic import BaseModel
class Address(BaseModel):
city: str
zip: str
class User(BaseModel):
id: int
name: str
address: Address # nested model
tags: list[str] # typed listArrays become list[...], and a nested object becomes its own BaseModel referenced by field type. user.address.city is fully typed all the way down.
Aliases: when JSON keys aren't valid Python
Real-world JSON often uses camelCase, reserved words, or hyphens that aren't legal Python identifiers. Pydantic's aliases bridge the gap so your Python stays idiomatic:
from pydantic import BaseModel, Field
class Event(BaseModel):
event_type: str = Field(alias="eventType")
schema_id: str = Field(alias="$schema")
model_config = {"populate_by_name": True}Now JSON with eventType and $schema populates the snake_case Python attributes, and model_dump_json(by_alias=True) serializes back to the original key names. This is essential when consuming an external API you don't control.
Why Pydantic dominates Python AI code
Pydantic's runtime validation is exactly what unreliable data sources need — and nothing is less reliable than an LLM. Libraries like Instructor and Pydantic AI use your BaseModel as the contract: the model's JSON output is validated against it, and on a mismatch the framework can re-prompt with the validation error until the data conforms. Define the model once and it doubles as your API schema, your LLM output schema, and your internal type. Pydantic can also emit a JSON Schema from any model via model_json_schema(), so the same class describes data across service boundaries too.
Tips for clean conversions
- Start from a representative sample, not one lucky record — include the nullable and edge-case fields so
Optionalis right. - Use aliases rather than renaming to match ugly external keys; keep Python readable.
- Prefer `model_validate_json` over
json.loadsthenmodel_validate— it's a single validated step. - Validate at the boundary. Convert JSON to a model the moment it enters your program, so everything downstream works with typed, trusted data.
For the plain-types version see JSON to Python, and for how Pydantic compares to Zod and JSON Schema see JSON Schema vs Zod vs Pydantic.