FastAPI Pydantic Models

Pydantic v2 model patterns for FastAPI: field constraints, validators, nested models, response schemas, and serialization configuration.

1. BaseModel Fundamentals

from pydantic import BaseModel, Field, EmailStr
from typing import Optional
from datetime import datetime
from uuid import UUID

class UserBase(BaseModel):
    email: EmailStr
    name: str = Field(..., min_length=1, max_length=100, examples=["Alice"])
    age: Optional[int] = Field(None, ge=0, le=150)
    role: str = Field(default="user", pattern="^(user|admin|moderator)$")

class UserCreate(UserBase):
    password: str = Field(..., min_length=8, max_length=128)

class UserResponse(UserBase):
    id: UUID
    created_at: datetime
    is_active: bool = True

    model_config = {"from_attributes": True}  # allows ORM model → Pydantic

# Usage in route
@app.post("/users", response_model=UserResponse, status_code=201)
async def create_user(user_in: UserCreate, db: DB):
    user = User(**user_in.model_dump(exclude={"password"}), hashed_password=hash(user_in.password))
    db.add(user)
    db.commit()
    db.refresh(user)
    return user

2. Field Validators (Pydantic v2)

from pydantic import BaseModel, field_validator, model_validator
import re

class RegisterRequest(BaseModel):
    username: str
    password: str
    confirm_password: str
    phone: Optional[str] = None

    @field_validator("username")
    @classmethod
    def username_alphanumeric(cls, v: str) -> str:
        if not re.match(r"^[a-zA-Z0-9_]{3,30}$", v):
            raise ValueError("Username must be 3-30 alphanumeric chars or underscores")
        return v.lower()

    @field_validator("phone")
    @classmethod
    def phone_format(cls, v: Optional[str]) -> Optional[str]:
        if v is None:
            return v
        cleaned = re.sub(r"[\s\-\(\)]", "", v)
        if not re.match(r"^\+?\d{7,15}$", cleaned):
            raise ValueError("Invalid phone number")
        return cleaned

    @model_validator(mode="after")
    def passwords_match(self) -> "RegisterRequest":
        if self.password != self.confirm_password:
            raise ValueError("Passwords do not match")
        return self

3. Nested Models

from pydantic import BaseModel
from typing import List

class Address(BaseModel):
    street: str
    city: str
    country: str = "CN"
    postal_code: Optional[str] = None

class Tag(BaseModel):
    id: int
    name: str
    color: str = "#000000"

class Article(BaseModel):
    title: str
    content: str
    author_id: int
    tags: List[Tag] = []
    address: Optional[Address] = None

# Nested validation happens automatically
article = Article(
    title="Hello",
    content="World",
    author_id=1,
    tags=[{"id": 1, "name": "python"}],
    address={"street": "123 Main St", "city": "Beijing"},
)

4. Response Models & Filtering

from pydantic import BaseModel, computed_field

class UserInDB(BaseModel):
    id: int
    email: str
    name: str
    hashed_password: str
    is_active: bool
    created_at: datetime
    model_config = {"from_attributes": True}

# Response model — excludes sensitive fields
class UserPublic(BaseModel):
    id: int
    name: str
    created_at: datetime
    model_config = {"from_attributes": True}

    @computed_field
    @property
    def member_since(self) -> str:
        return self.created_at.strftime("%B %Y")

# Paginated response
class Page(BaseModel):
    items: List[UserPublic]
    total: int
    page: int
    size: int
    pages: int

@app.get("/users", response_model=Page)
async def list_users(page: int = 1, size: int = 20, db: DB = Depends(get_db)):
    total = db.query(User).count()
    users = db.query(User).offset((page-1)*size).limit(size).all()
    return Page(items=users, total=total, page=page, size=size, pages=-(-total // size))

5. model_config Options

from pydantic import BaseModel, ConfigDict

class StrictModel(BaseModel):
    model_config = ConfigDict(
        str_strip_whitespace=True,    # strip whitespace from strings
        str_min_length=1,             # global min length for strings
        validate_assignment=True,     # validate on attribute assignment
        use_enum_values=True,         # store enum value not enum instance
        from_attributes=True,         # allow ORM object instantiation
        populate_by_name=True,        # allow both alias and field name
        frozen=False,                 # True = immutable model
        json_schema_extra={
            "example": {"name": "Alice", "age": 30}
        },
    )

6. Common Field Types

TypeImportNotes
EmailStrpydanticValidates email format
HttpUrlpydanticValidates URL format
UUIDuuidAccepts str or UUID
datetimedatetimeISO 8601 parsing
DecimaldecimalPrecise numeric
Literal["a","b"]typingEnum-like string
constr()pydanticConstrained string
conint()pydanticConstrained integer