Search Topics
Search across all FastAPI topics
Pydantic Models
Core ConceptPydantic is the data validation library that powers FastAPI. Models define the shape of your data with Python type annotations, giving you validation, serialization, and documentation for free.
You add email: str to your User model. Someone signs up with email: "not-an-email". It passes validation. Your database now has garbage data. Why didn't Pydantic catch this?
POST /users {"name": "Bob", "email": "not-an-email"}
→ 200 OK ✓ Created!
# Wait... "not-an-email" is not a valid email!
# But str accepts ANY string. Pydantic did exactly what you asked.Question
Frustrating, right? You expected Pydantic to validate emails, but you told it the type was str. And "not-an-email" is a valid string. Pydantic did exactly what you asked — the problem is you didn't ask for enough. This is where custom validators and specialized types come in.
The Validation Pipeline
When data hits a Pydantic model, every field goes through a validation pipeline. The type hint is just the first check. You can add more layers with Field(), validators, and specialized types.
Raw data arrives
{"email": "not-an-email"}
Type check
Is it a str? Yes
Field constraints
min_length? pattern?
Custom validators
@field_validator?
Model accepted
All checks passed
If you only declare email: str, only the type check runs. The more constraints you add, the more Pydantic validates for you.
Validation pipeline
What you just learned
Type hints are just the first layer of validation
Field() adds constraints like min_length, regex patterns, and more
Custom validators run after type checking for business-logic validation
Defining Models
Create models by subclassing BaseModel. Each field uses standard Python type annotations. Add Field() when you need constraints beyond the basic type.
from pydantic import BaseModel, Field
from datetime import datetime
class User(BaseModel):
id: int
name: str = Field(min_length=1, max_length=100)
email: str
is_active: bool = True
created_at: datetime = Field(default_factory=datetime.now)
tags: list[str] = []Default values make fields optional. is_active defaults to True, tags defaults to an empty list. Fields without defaults (id, name, email) are required.
Nested Models
Real-world data is rarely flat. Models can reference other models, and Pydantic validates at every level. If the nested address has a bad zip code, you'll know exactly where.
class Address(BaseModel):
street: str
city: str
country: str
zip_code: str
class User(BaseModel):
name: str
address: Address # nested model
shipping_addresses: list[Address] = [] # list of modelsModel structure
What you just learned
BaseModel subclasses define your data shape
Field() adds min/max length, patterns, and metadata
Nested models validate at every level with precise error paths
Fixing the Email Problem
Your User model has email: str and someone signs up with "not-an-email". How do you actually validate emails?
class User(BaseModel):
name: str
email: str # Accepts ANY string!
# POST {"name": "Bob", "email": "not-an-email"}
# → 200 OK ← This shouldn't pass!Custom Field Validators
The @field_validator decorator is your tool for business logic that goes beyond type checking. Want to ensure ages are reasonable? That passwords meet complexity requirements? Validators are the answer.
from pydantic import BaseModel, field_validator
class User(BaseModel):
name: str
email: str
age: int
@field_validator("email")
@classmethod
def validate_email(cls, v: str) -> str:
if "@" not in v:
raise ValueError("Invalid email address")
return v.lower()
@field_validator("age")
@classmethod
def validate_age(cls, v: int) -> int:
if v < 0 or v > 150:
raise ValueError("Age must be between 0 and 150")
return vInsight
Validators can do two things: reject bad data (raise ValueError) and transform good data (return a modified value). The email validator does both — it rejects strings without @ and lowercases valid emails.
Custom validation
What you just learned
@field_validator adds business logic validation beyond type checking
Validators can reject (raise ValueError) and transform (return modified value)
Validators run after type coercion, so you get the correct type
Try It Yourself
Edit the JSON below and hit Validate to see how Pydantic checks each field. Switch between models or click "break it" to see validation errors in action.
Think about it...
What's the difference between a Pydantic model and a Python dataclass for API validation?
Hint: Think about what happens when you pass the string '25' to a field declared as int.
Key Points
Type-Driven
Python type hints define validation, serialization, and docs
Nested Validation
Deeply nested structures are validated at every level
Custom Validators
Add business logic with @field_validator and @model_validator
model_dump()
Convert models to dicts with .model_dump() for serialization