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 v

Avoid 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 = None

Keep 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.