Overview
Pydantic v2 is the validation and serialization layer that FastAPI builds on. Every request body, response body, and query parameter set passes through a Pydantic model. Getting model design right prevents a category of bugs where internal state leaks to clients, client-supplied values corrupt server-side fields, or validation runs redundantly across layers. This page extends the model guidance in fastapi with Pydantic-specific patterns. See python-typing for typing conventions used throughout.
Use separate models for input and output
One model per data direction. The model that accepts input is not the model that returns output.
from pydantic import BaseModel, EmailStr
from datetime import datetime
class UserCreate(BaseModel):
email: EmailStr
password: str
class UserRead(BaseModel):
id: str
email: EmailStr
created_at: datetime
model_config = {"from_attributes": True}Write models (UserCreate, OrderUpdate) accept exactly what the caller may supply. Response models (UserRead) expose exactly what the caller may see. Fields like password_hash, internal_status, and role_flags belong on neither. Set model_config = {"from_attributes": True} on response models so they can be constructed from ORM instances. Register both in the router via response_model=UserRead rather than returning dicts.
Validate at the edge, not in every layer
Run Pydantic validation once, at the HTTP boundary. Do not re-validate the same data in the service layer, the repository layer, or the database helper. Pass typed model instances or plain Python dataclasses between layers; re-validating adds overhead and masks the origin of bad data.
@router.post("/users", response_model=UserRead, status_code=201)
async def create_user(payload: UserCreate, db: AsyncSession = Depends(get_db)) -> UserRead:
user = await user_service.create(db, payload) # payload is already validated
return UserRead.model_validate(user)See fastapi-dependencies for passing validated payloads through the dependency chain.
Use field validators for business rules, not type coercion
@field_validator runs after Pydantic has coerced the raw value to the annotated type. Use it for business rules that go beyond type correctness.
from pydantic import BaseModel, field_validator
class OrderCreate(BaseModel):
quantity: int
unit_price: float
@field_validator("quantity")
@classmethod
def quantity_must_be_positive(cls, v: int) -> int:
if v <= 0:
raise ValueError("quantity must be positive")
return vAvoid validators that duplicate type annotation work, such as checking isinstance(v, str) when the field is already str. Use @model_validator(mode="after") for cross-field validation where one field constrains another.
Prefer aliases for external names that differ from Python conventions
API consumers often expect camelCase or hyphenated field names. Use alias or alias_generator rather than naming Python attributes after external conventions.
from pydantic import BaseModel, Field
from pydantic.alias_generators import to_camel
class InvoiceRead(BaseModel):
invoice_id: str
line_item_count: int
total_amount_cents: int
model_config = {
"alias_generator": to_camel,
"populate_by_name": True,
}Set populate_by_name: True so that internal code can still use snake_case attribute names. The FastAPI route will serialize responses using the alias automatically. Explicitly set by_alias=True if you call model.model_dump() manually.
Compose nested models for structured payloads
Model nested structures as separate classes rather than dicts or Any. Nesting keeps schemas focused and lets OpenAPI render the structure correctly. See fastapi-openapi for how nested models appear in the generated schema.
class Address(BaseModel):
street: str
city: str
postal_code: str
class CustomerCreate(BaseModel):
name: str
email: EmailStr
billing_address: Address
shipping_address: Address | None = NoneKeep individual models under about ten fields. When a model grows beyond that, split it into sub-models that each capture a coherent domain concept. The postgres schema should guide the nesting: models that mirror table relationships are easier to validate and serialize.
Use model_json_schema and model_validate_json for raw JSON paths
When reading JSON from a message queue, a file, or a webhook without FastAPI’s automatic parsing, use Pydantic’s own methods rather than json.loads followed by manual construction.
import json
from pydantic import BaseModel
class WebhookEvent(BaseModel):
event_type: str
payload: dict[str, object]
raw = b'{"event_type": "payment.success", "payload": {"amount": 500}}'
event = WebhookEvent.model_validate_json(raw)model_validate_json is faster than model_validate(json.loads(raw)) because it parses and validates in one pass. Use model_json_schema() to export the schema for documentation or contract testing without spinning up the full FastAPI application.