Pydantic is the engine behind every piece of data validation in FastAPI. When a client sends a POST request, Pydantic parses the JSON, checks every field against its type and constraints, and either creates a validated Python object or returns a detailed error response—all before your route handler runs a single line of code. On the response side, Pydantic serializes your return data and filters out fields that should not be exposed to the client. This guide covers how Pydantic models work in FastAPI from the ground up: defining schemas, adding field constraints, writing custom validators, handling nested data, separating request models from response models, and using the response_model parameter to control what your API sends back.
How Pydantic Models Fit Into FastAPI
When you declare a route handler parameter with a Pydantic model type, FastAPI does the following behind the scenes: it reads the raw request body, parses it as JSON into a Python dictionary, and passes that dictionary to the Pydantic model's constructor. Pydantic checks every field for the correct type, applies any constraints you defined, runs custom validators if present, and either produces a validated model instance or raises a validation error. If validation fails, FastAPI returns a 422 Unprocessable Entity response with a detailed list of what went wrong. Your handler code never executes.
This means that by the time your function runs, the data is guaranteed to match the schema you defined. You do not need to write manual checks like "if field is missing" or "if field is not an integer." Pydantic handles all of it.
Defining a Basic Schema
A Pydantic model is a class that inherits from BaseModel. Each attribute defines a field with a type annotation. Required fields have no default value. Optional fields use None as a default.
from pydantic import BaseModel
class ItemCreate(BaseModel):
title: str
description: str | None = None
price: float
is_available: bool = True
To use this model as a request body in FastAPI:
from fastapi import FastAPI
app = FastAPI()
@app.post("/items/")
def create_item(item: ItemCreate):
return {"title": item.title, "price": item.price}
FastAPI reads the JSON body and validates it against ItemCreate. The title and price fields are required. The description field defaults to None if not provided. The is_available field defaults to True. If the client sends a string where price expects a float, FastAPI returns a 422 error before the handler runs.
Field Constraints With Field()
Pydantic's Field() function lets you add validation constraints, descriptions, and metadata directly on model attributes. These constraints are enforced during validation and also appear in the auto-generated Swagger UI documentation.
from pydantic import BaseModel, Field
class ItemCreate(BaseModel):
title: str = Field(
...,
min_length=1,
max_length=200,
description="The name of the item",
)
description: str | None = Field(
default=None,
max_length=1000,
description="Optional description",
)
price: float = Field(
...,
gt=0,
description="Price must be greater than zero",
)
quantity: int = Field(
default=1,
ge=1,
le=10000,
description="Number of units (1 to 10,000)",
)
The ellipsis (...) means the field is required with no default. The constraint parameters—gt (greater than), ge (greater than or equal), le (less than or equal), min_length, max_length—are checked automatically during validation.
| Constraint | Applies To | Meaning |
|---|---|---|
gt | Numbers | Greater than |
ge | Numbers | Greater than or equal |
lt | Numbers | Less than |
le | Numbers | Less than or equal |
min_length | Strings, Lists | Minimum length |
max_length | Strings, Lists | Maximum length |
pattern | Strings | Regex pattern |
Custom Validators: field_validator and model_validator
When built-in constraints are not enough, Pydantic v2 provides field_validator for single-field rules and model_validator for rules that depend on multiple fields.
Single-Field Validation
from pydantic import BaseModel, field_validator
class UserCreate(BaseModel):
username: str
email: str
password: str
@field_validator("email")
@classmethod
def email_must_contain_at(cls, v: str) -> str:
if "@" not in v:
raise ValueError("Invalid email format")
return v.lower()
@field_validator("password")
@classmethod
def password_strength(cls, v: str) -> str:
if len(v) < 8:
raise ValueError("Password must be at least 8 characters")
return v
Each validator receives the field value, checks it, and returns it (optionally transformed). The email validator normalizes the value to lowercase. If validation fails, Pydantic includes the error message in the 422 response.
Cross-Field Validation
from pydantic import BaseModel, model_validator
class DateRange(BaseModel):
start_date: str
end_date: str
@model_validator(mode="after")
def end_after_start(self):
if self.end_date <= self.start_date:
raise ValueError("end_date must be after start_date")
return self
A model_validator with mode="after" runs after all individual fields have been validated. It receives the fully constructed model instance and can compare fields against each other.
In Pydantic v2, validators use the @field_validator and @model_validator decorators. The older @validator decorator from v1 still works but is deprecated. Use the v2 syntax for new code.
Nested Models
Pydantic models can contain other Pydantic models, allowing you to validate complex, nested JSON structures.
from pydantic import BaseModel
class Address(BaseModel):
street: str
city: str
state: str
zip_code: str
class UserCreate(BaseModel):
name: str
email: str
address: Address
The client sends:
{
"name": "Kandi Brian",
"email": "kandi@example.com",
"address": {
"street": "123 Main St",
"city": "Springfield",
"state": "IL",
"zip_code": "62704"
}
}
FastAPI and Pydantic validate both the outer UserCreate model and the inner Address model. If the zip_code is missing, the 422 error will point to the exact nested location of the problem.
Separate Schemas for Create, Update, and Response
A common pattern is to define a base schema with shared fields, then extend it for different purposes. This avoids duplicating field definitions while keeping each schema tailored to its role.
from pydantic import BaseModel, ConfigDict
class ItemBase(BaseModel):
title: str
description: str | None = None
price: float
class ItemCreate(ItemBase):
"""Fields the client sends when creating an item."""
pass
class ItemUpdate(BaseModel):
"""Partial update: all fields optional."""
title: str | None = None
description: str | None = None
price: float | None = None
class ItemOut(ItemBase):
"""Fields returned to the client. Includes database-generated values."""
id: int
owner_id: int
model_config = ConfigDict(from_attributes=True)
ItemCreate inherits required fields from ItemBase. ItemUpdate makes every field optional for partial updates. ItemOut adds id and owner_id which come from the database. The client never sends those fields; they only appear in responses.
Do not use the same model for both request input and response output. A UserCreate schema includes a plain-text password. If you return that same model in the response, you expose the password to the client. Always define a separate response schema that excludes sensitive fields.
Controlling Responses With response_model
The response_model parameter on a route decorator tells FastAPI to validate and filter the response data before sending it to the client.
@app.post("/items/", response_model=ItemOut, status_code=201)
def create_item(item: ItemCreate):
# Imagine this returns a database object with extra fields
db_item = save_to_database(item)
return db_item
Even if db_item contains fields like hashed_password or internal_notes that are not part of ItemOut, FastAPI strips them from the response. Only the fields defined in the response model reach the client. This is a powerful safety net against accidentally exposing internal data.
The response model also drives the Swagger UI documentation. Consumers of your API see exactly which fields to expect in the response, with types and descriptions included.
Reading From ORM Objects With from_attributes
By default, Pydantic expects a dictionary when creating a model instance. If you return a SQLAlchemy model object from your route handler, Pydantic needs to be told to read data from object attributes instead. In Pydantic v2, this is done with ConfigDict(from_attributes=True).
from pydantic import BaseModel, ConfigDict
class UserOut(BaseModel):
id: int
name: str
email: str
model_config = ConfigDict(from_attributes=True)
This replaces the older Pydantic v1 syntax of class Config: orm_mode = True. With from_attributes=True, you can return a SQLAlchemy object directly from your route and FastAPI will serialize it using the Pydantic model.
In Pydantic v2, model_config = ConfigDict(...) replaces the inner class Config pattern from v1. Both still work, but model_config is the recommended approach for new code.
Adding Examples for Swagger UI
You can embed example payloads directly in your Pydantic model. These examples populate the Swagger UI, making it easy for API consumers to test endpoints without reading external documentation.
from pydantic import BaseModel, Field, ConfigDict
class ItemCreate(BaseModel):
title: str = Field(..., min_length=1, max_length=200)
price: float = Field(..., gt=0)
model_config = ConfigDict(
json_schema_extra={
"examples": [
{
"title": "Wireless Keyboard",
"price": 49.99,
}
]
}
)
The json_schema_extra dictionary merges into the generated JSON Schema, and FastAPI's Swagger UI picks up the examples automatically. Clients can click "Try it out" and see a realistic payload pre-filled.
Frequently Asked Questions
What is the difference between a Pydantic model and a SQLAlchemy model in FastAPI?
A Pydantic model defines the shape of data crossing your API boundary—what the client sends in a request and what the server sends back in a response. A SQLAlchemy model defines how data is stored in your database. They often have overlapping fields, but they serve different purposes and should be kept separate so that your API contract stays independent from your database structure.
How does FastAPI validate request data with Pydantic?
FastAPI reads the raw JSON body, parses it into a dictionary, and passes it to the Pydantic model's constructor. Pydantic checks every field for type correctness, applies constraints (like gt=0 or max_length=200), runs custom validators, and either produces a validated instance or raises a validation error. If validation fails, FastAPI returns a 422 response with detailed error messages. Your route handler never executes.
What is response_model in FastAPI and why should I use it?
The response_model parameter tells FastAPI to validate and filter the response before sending it to the client. It strips out any fields not defined in the response schema, preventing accidental exposure of internal data. It also drives the response documentation in Swagger UI, showing API consumers exactly which fields to expect.
What replaced orm_mode in Pydantic v2?
In Pydantic v2, orm_mode = True inside class Config has been replaced by model_config = ConfigDict(from_attributes=True). This tells Pydantic to read data from object attributes (like SQLAlchemy model instances) instead of requiring dictionaries.
Key Takeaways
- Pydantic validates before your code runs: Every request body is parsed, type-checked, and constraint-validated before the route handler executes. Invalid data gets a 422 response with clear error details.
- Use Field() for built-in constraints:
gt,ge,lt,le,min_length,max_length, andpatternhandle the common cases. These constraints also appear in the Swagger UI documentation. - Use field_validator and model_validator for custom rules: Single-field validation uses
@field_validator. Cross-field validation uses@model_validator(mode="after"). Both are Pydantic v2 syntax. - Separate schemas for different purposes: Define a base model with shared fields, then extend it into Create, Update, and Response schemas. Never return the same model used for input—response schemas should exclude sensitive fields.
- Use response_model to control output: The
response_modelparameter filters out extra fields, validates response data, and generates accurate Swagger documentation. It is your safety net against exposing internal data.
Pydantic is not an add-on to FastAPI—it is the foundation that makes the framework work. Every type hint on a route parameter, every Field constraint, and every custom validator becomes part of a validation pipeline that runs automatically on every request. The result is an API where invalid data never reaches your business logic, responses never leak internal fields, and the documentation is always accurate because it is generated from the same schemas your code enforces.