Pydantic is responsible for converting untrusted input into validated Python objects while ensuring that application data conforms to the expected schema.
The Problem Pydantic Solves
Imagine an API that creates a new user. Without validation, an endpoint might look like this:@app.post("/users")
async def create_user(user: dict):
return user
Suppose the client sends:
{
"name": 100,
"age": "twenty"
}
The endpoint receives the data exactly as provided.
Without validation, every endpoint must manually verify:
- Does the name exist?
- Is the name a string?
- Is age present?
- Is age an integer?
- Is age within an acceptable range?
Pydantic solves this problem by validating incoming data before endpoint execution.
Creating Your First Pydantic Model
A Pydantic model represents the structure of the data your API expects.from pydantic import BaseModel
class UserRequest(BaseModel):
name: str
age: int
The endpoint now becomes:
@app.post("/users")
async def create_user(user: UserRequest):
return user
If the client sends:
{
"name": "John",
"age": 30
}
FastAPI automatically creates a UserRequest object and passes it to the endpoint. The endpoint receives a fully validated Python object instead of raw JSON.
What Happens Internally?
When a request reaches FastAPI, Pydantic validation occurs before the endpoint executes.
If validation fails, endpoint execution never begins. Instead, FastAPI immediately returns a 422 Unprocessable Entity response.
Field Validation Using field_validator()
Pydantic V2 replaces the old@validator decorator with @field_validator. Suppose user names must contain at least three characters.
from pydantic import BaseModel
from pydantic import field_validator
class UserRequest(BaseModel):
name: str
age: int
@field_validator("name")
@classmethod
def validate_name(cls, value):
if len(value) < 3:
raise ValueError(
"Name must contain at least 3 characters"
)
return value
Whenever a request arrives, the validator executes automatically. If validation fails, FastAPI returns a validation error without executing the endpoint.
Model Validation Using model_validator()
Sometimes validation depends on multiple fields. Suppose minors cannot register with a company email address.from pydantic import BaseModel
from pydantic import model_validator
class UserRequest(BaseModel):
age: int
email: str
@model_validator(mode="after")
def validate_user(self):
if self.age < 18 and self.email.endswith("@company.com"):
raise ValueError(
"Minors cannot use company email addresses."
)
return self
Unlike field validators, model validators have access to the complete object. This makes them suitable for cross-field validation.
Field Constraints
Many common validation rules can be declared without writing custom validators.from pydantic import BaseModel
from pydantic import Field
class UserRequest(BaseModel):
name: str = Field(
min_length=3,
max_length=50
)
age: int = Field(
ge=18,
le=100
)
Pydantic automatically validates these constraints. No additional code is required.
Response Models
Pydantic is also used when returning responses.class UserResponse(BaseModel):
id: int
name: str
@app.get(
"/users/{id}",
response_model=UserResponse
)
async def get_user(id: int):
return {
"id": id,
"name": "John",
"password": "secret"
}
Although the endpoint returns a password field, FastAPI excludes it from the response because it is not defined in the response model. This prevents accidental exposure of sensitive data.
Using response models is one of the simplest ways to protect sensitive information in production APIs.
Serializing Models
Pydantic V2 replacesdict() with model_dump().
user = UserRequest(
name="John",
age=30
)
print(user.model_dump())
Output:
{
"name": "John",
"age": 30
}
Whenever data needs to be converted back into a dictionary, model_dump() should be used.
Creating Models from Existing Data
Pydantic V2 also introducesmodel_validate().
data = {
"name": "John",
"age": 30
}
user = UserRequest.model_validate(data)
Instead of manually constructing the object, Pydantic validates the input and creates the model. This method is commonly used when processing data from external systems.
Nested Models
Production APIs frequently contain nested objects.class Address(BaseModel):
city: str
country: str
class UserRequest(BaseModel):
name: str
address: Address
Incoming JSON:
{
"name": "John",
"address": {
"city": "London",
"country": "UK"
}
}
Pydantic recursively validates nested objects, ensuring every level of the request conforms to the expected schema.
Validation Errors
Suppose the client sends:{
"name": "Jo",
"age": "abc"
}
Pydantic identifies all validation errors before the endpoint executes.
{
"detail": [
...
]
}
The endpoint is never executed. This prevents invalid data from reaching business logic.
In production systems, Pydantic models should represent API contracts rather than database entities. Separate models are typically created for:Using dedicated request and response models makes APIs easier to evolve without affecting internal implementation.UserCreateRequest UserUpdateRequest UserResponse UserSummary UserDetails
Conclusion
Pydantic V2 is the foundation of FastAPI's request validation, response serialization, schema generation, and type safety.By validating incoming data before endpoint execution, supporting custom validation rules, and generating well-defined API contracts, it helps developers build APIs that are reliable, maintainable, and secure.