Pydantic v2 is a complete rewrite in Rust with a Python API — 5-50x faster than v1 for validation and serialization. The @field_validator, @model_validator, and @computed_field decorators provide fine-grained control over validation logic. pydantic-settings manages typed environment configuration with .env file loading. Claude Code generates Pydantic models, validators, settings classes, and the integration patterns for FastAPI and SQLAlchemy.
CLAUDE.md for Pydantic Projects
## Data Validation Stack
- Pydantic v2.x (not v1 — validators use @field_validator not @validator)
- pydantic-settings for environment config
- FastAPI with native Pydantic v2 integration
- SQLAlchemy 2.x with pydantic-sqlalchemy for ORM ↔ Pydantic bridges
- Serialization: use model.model_dump() not model.dict() (v2 API)
- Validation: use model_validate() not parse_obj() (v2 API)
- JSON schema: use model.model_json_schema() not schema() (v2 API)
Model Definition with Field Validators
# models/order.py
from __future__ import annotations
from datetime import datetime, timezone
from decimal import Decimal
from enum import StrEnum
from typing import Annotated
from pydantic import BaseModel, Field, field_validator, model_validator, computed_field, ConfigDict
import re
class OrderStatus(StrEnum):
PENDING = "pending"
PROCESSING = "processing"
SHIPPED = "shipped"
DELIVERED = "delivered"
CANCELLED = "cancelled"
# Reusable annotated types
PositiveDecimal = Annotated[Decimal, Field(gt=0)]
NonEmptyStr = Annotated[str, Field(min_length=1, max_length=500)]
class OrderItemCreate(BaseModel):
product_id: str = Field(pattern=r'^prod_[a-zA-Z0-9]+$')
quantity: int = Field(ge=1, le=100)
unit_price: PositiveDecimal
@computed_field
@property
def subtotal(self) -> Decimal:
return self.unit_price * self.quantity
class CreateOrderRequest(BaseModel):
model_config = ConfigDict(
str_strip_whitespace=True,
str_to_lower=False,
validate_default=True,
)
customer_email: str = Field(max_length=255)
shipping_address: str = Field(min_length=10, max_length=1000)
items: list[OrderItemCreate] = Field(min_length=1, max_length=50)
coupon_code: str | None = Field(default=None, max_length=20)
notes: NonEmptyStr | None = None
@field_validator('customer_email')
@classmethod
def validate_email(cls, v: str) -> str:
# More thorough than pydantic's EmailStr for custom domains
pattern = r'^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$'
if not re.match(pattern, v):
raise ValueError(f'Invalid email address: {v}')
return v.lower()
@field_validator('coupon_code')
@classmethod
def normalize_coupon(cls, v: str | None) -> str | None:
if v is None:
return None
return v.upper().strip()
@field_validator('items')
@classmethod
def validate_unique_products(cls, items: list[OrderItemCreate]) -> list[OrderItemCreate]:
product_ids = [item.product_id for item in items]
if len(product_ids) != len(set(product_ids)):
raise ValueError('Duplicate product IDs in order items')
return items
@computed_field
@property
def total_amount(self) -> Decimal:
return sum(item.subtotal for item in self.items)
@model_validator(mode='after')
def validate_order(self) -> CreateOrderRequest:
# Cross-field validation: high-value orders require notes
if self.total_amount > 1000 and self.notes is None:
raise ValueError('Orders over $1000 require notes for review')
return self
class OrderResponse(BaseModel):
model_config = ConfigDict(from_attributes=True) # Enable ORM mode
id: str
customer_email: str
status: OrderStatus
total_amount: Decimal
created_at: datetime
updated_at: datetime
items: list[OrderItemCreate]
@field_validator('created_at', 'updated_at', mode='before')
@classmethod
def ensure_utc(cls, v: datetime) -> datetime:
if v.tzinfo is None:
return v.replace(tzinfo=timezone.utc)
return v
pydantic-settings for Configuration
# config/settings.py
from pydantic import Field, PostgresDsn, RedisDsn, SecretStr, field_validator
from pydantic_settings import BaseSettings, SettingsConfigDict
from functools import lru_cache
class DatabaseSettings(BaseSettings):
url: PostgresDsn
pool_size: int = Field(default=10, ge=1, le=100)
max_overflow: int = Field(default=20, ge=0, le=200)
echo: bool = False
class RedisSettings(BaseSettings):
url: RedisDsn = Field(default="redis://localhost:6379/0")
max_connections: int = 20
class StripeSettings(BaseSettings):
secret_key: SecretStr # Never logged, never serialized
webhook_secret: SecretStr
publishable_key: str
class Settings(BaseSettings):
model_config = SettingsConfigDict(
env_file='.env',
env_file_encoding='utf-8',
env_nested_delimiter='__', # DATABASE__URL=... maps to database.url
case_sensitive=False,
extra='ignore',
)
# App
app_name: str = "My App"
environment: str = Field(default="development", pattern=r'^(development|staging|production)$')
debug: bool = False
secret_key: SecretStr
# Nested settings via env prefix
database: DatabaseSettings
redis: RedisSettings
stripe: StripeSettings
# Computed
@property
def is_production(self) -> bool:
return self.environment == "production"
@field_validator('debug')
@classmethod
def no_debug_in_production(cls, v: bool, info) -> bool:
# Note: cross-field validation in settings uses model_validator
return v
@lru_cache
def get_settings() -> Settings:
return Settings()
# .env — loaded automatically
ENVIRONMENT=production
SECRET_KEY=your-secret-key-here
DEBUG=false
DATABASE__URL=postgresql+asyncpg://user:pass@localhost:5432/mydb
DATABASE__POOL_SIZE=20
REDIS__URL=redis://localhost:6379/0
STRIPE__SECRET_KEY=sk_live_...
STRIPE__WEBHOOK_SECRET=whsec_...
STRIPE__PUBLISHABLE_KEY=pk_live_...
Serialization Modes
# Pydantic v2 serialization control
from pydantic import BaseModel, field_serializer, model_serializer
from decimal import Decimal
from datetime import datetime
class OrderSummary(BaseModel):
id: str
amount: Decimal
created_at: datetime
internal_note: str # Should not appear in API responses
@field_serializer('amount')
def serialize_amount(self, v: Decimal) -> str:
# Always serialize Decimal as string to avoid float precision issues
return str(v.quantize(Decimal('0.01')))
@field_serializer('created_at')
def serialize_datetime(self, v: datetime) -> str:
return v.isoformat()
# Serialization modes
order = OrderSummary(id="ord_123", amount=Decimal("99.99"), created_at=datetime.now(), internal_note="debug")
# Default: include all fields
order.model_dump()
# {'id': 'ord_123', 'amount': '99.99', 'created_at': '2026-11-22T...', 'internal_note': 'debug'}
# Exclude specific fields
order.model_dump(exclude={'internal_note'})
# Include only specific fields
order.model_dump(include={'id', 'amount'})
# JSON serialization (uses field_serializer)
order.model_dump_json(exclude={'internal_note'})
# Round-trip: dict → model
OrderSummary.model_validate(some_dict)
# From ORM object (with from_attributes=True in model_config)
OrderSummary.model_validate(sqlalchemy_orm_object)
JSON Schema Generation
# Generate JSON Schema for API documentation or validation
from pydantic import BaseModel
schema = CreateOrderRequest.model_json_schema()
# Returns full JSON Schema with all constraints, descriptions, examples
# Custom schema with examples
class CreateOrderRequest(BaseModel):
customer_email: str = Field(
...,
json_schema_extra={
"example": "[email protected]",
"x-order": 1, # Custom OpenAPI extension
}
)
# Schema for OpenAPI spec
print(CreateOrderRequest.model_json_schema(mode='serialization')) # For responses
print(CreateOrderRequest.model_json_schema(mode='validation')) # For requests
FastAPI Integration
# routers/orders.py — FastAPI uses Pydantic v2 natively
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.ext.asyncio import AsyncSession
router = APIRouter(prefix="/orders", tags=["orders"])
@router.post(
"/",
response_model=OrderResponse,
status_code=status.HTTP_201_CREATED,
responses={
422: {"description": "Validation error"},
409: {"description": "Conflict — duplicate order"},
},
)
async def create_order(
body: CreateOrderRequest, # Validated by Pydantic automatically
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
) -> OrderResponse:
# body is fully validated, typed, and cleaned
# body.customer_email is lowercase, body.coupon_code is uppercase
try:
order = await order_service.create(db, body, user_id=current_user.id)
except DuplicateOrderError as e:
raise HTTPException(status_code=409, detail=str(e))
# model_validate enables ORM mode — SQLAlchemy → Pydantic
return OrderResponse.model_validate(order)
# Custom validation error format
from fastapi import Request
from fastapi.responses import JSONResponse
from pydantic import ValidationError
@app.exception_handler(ValidationError)
async def validation_error_handler(request: Request, exc: ValidationError) -> JSONResponse:
return JSONResponse(
status_code=422,
content={
"error": "Validation failed",
"details": exc.errors(include_url=False), # v2: include_url=False for cleaner output
},
)
For the FastAPI routing and dependency injection that wraps these Pydantic models, see the FastAPI guide for complete endpoint patterns. For validating API responses in tests, the API testing guide covers using Pydantic schemas as test assertions. The Claude Skills 360 bundle includes Pydantic v2 skill sets covering validators, settings management, and FastAPI integration patterns. Start with the free tier to try Pydantic model generation.