Pydantic v2 Models
All data structures use Pydantic models. No raw dicts, no Any types.
Core Principles
- No raw dicts — always use Pydantic models
- No
Anytype — explicit types everywhere - Validate at boundaries — validate on input, trust internally
- Immutability preferred — use
frozen=Truewhere possible
Model Categories
Domain Model
from datetime import datetime
from enum import Enum
from pydantic import BaseModel, Field, ConfigDict
class UserRole(str, Enum):
ADMIN = "admin"
USER = "user"
GUEST = "guest"
class User(BaseModel):
model_config = ConfigDict(
validate_assignment=True,
extra="forbid",
use_enum_values=True,
str_strip_whitespace=True,
)
id: int = Field(..., gt=0)
name: str = Field(..., min_length=1, max_length=100)
email: str = Field(..., pattern=r"^[^@]+@[^@]+\.[^@]+$")
role: UserRole = UserRole.USER
created_at: datetime = Field(default_factory=datetime.utcnow)
tags: list[str] = Field(default_factory=list)Configuration (Settings)
from pydantic import Field, model_validator
from pydantic_settings import BaseSettings, SettingsConfigDict
class AppConfig(BaseSettings):
model_config = SettingsConfigDict(
env_prefix="APP_",
env_file=".env",
extra="ignore",
case_sensitive=False,
)
api_key: str = Field(..., min_length=32)
database_url: str
debug: bool = False
timeout: int = Field(default=30, gt=0, le=300)
workers: int = Field(default=4, ge=1, le=32)
@model_validator(mode="after")
def validate_settings(self) -> "AppConfig":
if self.debug and self.workers > 1:
object.__setattr__(self, "workers", 1)
return selfRequest / Response
from typing import Generic, TypeVar
T = TypeVar("T")
class PaginationParams(BaseModel):
page: int = Field(default=1, ge=1)
per_page: int = Field(default=20, ge=1, le=100)
class ApiResponse(BaseModel, Generic[T]):
success: bool
data: T | None = None
error: str | None = None
class PaginatedResponse(BaseModel, Generic[T]):
items: list[T]
total: int
page: int
per_page: int
total_pages: int
@property
def has_next(self) -> bool:
return self.page < self.total_pagesValidation
Field Validators
from pydantic import field_validator
class UserCreate(BaseModel):
name: str
email: str
password: str
@field_validator("name")
@classmethod
def validate_name(cls, v: str) -> str:
if not v.strip():
raise ValueError("Name cannot be empty")
return v.strip().title()
@field_validator("email")
@classmethod
def validate_email(cls, v: str) -> str:
v = v.lower().strip()
if "@" not in v:
raise ValueError("Invalid email format")
return vModel Validators (Cross-Field)
from pydantic import model_validator
class DateRange(BaseModel):
start_date: datetime
end_date: datetime
@model_validator(mode="after")
def validate_dates(self) -> "DateRange":
if self.start_date >= self.end_date:
raise ValueError("start_date must be before end_date")
return selfBefore Validation
class FlexibleInput(BaseModel):
value: int
@model_validator(mode="before")
@classmethod
def parse_input(cls, data: Any) -> dict:
if isinstance(data, (int, str)):
return {"value": int(data)}
return dataSerialization
user = User(id=1, name="John", email="[email protected]")
# Basic
data = user.model_dump()
# Exclude None values
data = user.model_dump(exclude_none=True)
# Specific fields only
data = user.model_dump(include={"id", "name"})
# JSON-safe types (datetime → ISO string)
data = user.model_dump(mode="json")
# Direct JSON string
json_str = user.model_dump_json()Custom Serialization
from pydantic import field_serializer
from decimal import Decimal
class Product(BaseModel):
name: str
price: Decimal
@field_serializer("price")
def serialize_price(self, value: Decimal) -> str:
return f"${value:.2f}"Computed Fields
from pydantic import computed_field
class Rectangle(BaseModel):
width: float
height: float
@computed_field
@property
def area(self) -> float:
return self.width * self.height
rect = Rectangle(width=10, height=5)
rect.model_dump() # {"width": 10, "height": 5, "area": 50.0}Discriminated Unions
from typing import Literal, Annotated, Union
class Cat(BaseModel):
pet_type: Literal["cat"] = "cat"
meows: int
class Dog(BaseModel):
pet_type: Literal["dog"] = "dog"
barks: float
Pet = Annotated[Union[Cat, Dog], Field(discriminator="pet_type")]
class Owner(BaseModel):
name: str
pet: PetImmutable Models
class ImmutableUser(BaseModel):
model_config = ConfigDict(frozen=True)
id: int
name: str
email: str
user = ImmutableUser(id=1, name="John", email="[email protected]")
user.name = "Jane" # FrozenInstanceError!v1 to v2 Migration
| v1 | v2 | Notes |
|---|---|---|
.parse_obj(x) | .model_validate(x) | Main validation |
.dict() | .model_dump() | Serialization |
.json() | .model_dump_json() | JSON output |
.parse_raw(s) | .model_validate_json(s) | JSON parsing |
.schema() | .model_json_schema() | JSON schema |
@validator | @field_validator | Field validation |
@root_validator | @model_validator | Model validation |
Config inner class | model_config = ConfigDict() | Configuration |
__fields__ | model_fields | Field access |
.copy() | .model_copy() | Copying |
Anti-Patterns
# BAD — raw dict
def process(data: dict) -> dict:
return {"result": data["value"]}
# BAD — Any type
def process(data: Any) -> Any:
return data
# BAD — dict field in model
class BadModel(BaseModel):
data: dict # Use nested Pydantic model!
# GOOD — nested model
class NestedData(BaseModel):
key: str
value: int
class GoodModel(BaseModel):
data: NestedDataRules
- No raw dicts — every data structure is a Pydantic model
- No
Anytype — explicit types for every field - Validate at boundaries —
model_validate()at input, trust internally - Use
ConfigDict— not innerConfigclass (v2 syntax) model_dump()not.dict()— use v2 methods only@field_validatornot@validator— use v2 decorators- Immutable where possible —
frozen=Truefor value objects - Constraints on fields —
min_length,gt,le,pattern
Last updated on