| name | pydantic-models |
| description | Automatically applies when creating data models for API responses and validation. Uses Pydantic BaseModel with validators, field definitions, and proper serialization. |
Pydantic Model Pattern Enforcer
When creating data models for API responses, validation, or configuration, follow Pydantic best practices.
✅ Correct Pattern
from pydantic import BaseModel, Field, field_validator, model_validator
from typing import Optional, List
from datetime import datetime
class User(BaseModel):
"""User model with validation."""
id: str = Field(..., description="User unique identifier")
email: str = Field(..., description="User email address")
name: str = Field(..., min_length=1, max_length=100)
age: Optional[int] = Field(None, ge=0, le=150)
created_at: datetime = Field(default_factory=datetime.now)
tags: List[str] = Field(default_factory=list)
@field_validator('email')
@classmethod
def validate_email(cls, v: str) -> str:
"""Validate email format."""
if '@' not in v:
raise ValueError('Invalid email format')
return v.lower()
@field_validator('name')
@classmethod
def validate_name(cls, v: str) -> str:
"""Validate and clean name."""
return v.strip()
@model_validator(mode='after')
def validate_model(self) -> 'User':
"""Cross-field validation."""
if self.age and self.age < 13:
if '@' in self.email and not self.email.endswith('@parent.com'):
raise ValueError('Users under 13 must use parent email')
return self
class Config:
"""Pydantic configuration."""
json_schema_extra = {
"example": {
"id": "usr_123",
"email": "user@example.com",
"name": "John Doe",
"age": 30,
"tags": ["premium", "verified"]
}
}
Field Definitions
from pydantic import BaseModel, Field, HttpUrl, EmailStr
from typing import Annotated
from decimal import Decimal
class Product(BaseModel):
"""Product with comprehensive field validation."""
name: str = Field(..., min_length=1, max_length=200)
description: str = Field("", max_length=1000)
price: Decimal = Field(..., ge=0, decimal_places=2)
quantity: int = Field(0, ge=0)
sku: str = Field(..., pattern=r'^[A-Z0-9-]+$')
image_url: Optional[HttpUrl] = None
contact_email: Optional[EmailStr] = None
weight: Annotated[float, Field(gt=0, description="Weight in kg")]
status: Literal["active", "inactive", "discontinued"] = "active"
Field Validators
from pydantic import BaseModel, field_validator
import re
class PhoneContact(BaseModel):
phone: str
country_code: str = "US"
@field_validator('phone')
@classmethod
def validate_phone(cls, v: str) -> str:
"""Validate and normalize phone number."""
digits = re.sub(r'\D', '', v)
if len(digits) != 10:
raise ValueError('Phone must be 10 digits')
return digits
@field_validator('country_code')
@classmethod
def validate_country(cls, v: str) -> str:
"""Validate country code."""
allowed = ['US', 'CA', 'UK']
if v not in allowed:
raise ValueError(f'Country must be one of {allowed}')
return v
Model Validators
from pydantic import BaseModel, model_validator
from datetime import datetime
class Booking(BaseModel):
start_date: datetime
end_date: datetime
guests: int
@model_validator(mode='after')
def validate_dates(self) -> 'Booking':
"""Validate date range."""
if self.end_date <= self.start_date:
raise ValueError('end_date must be after start_date')
duration = (self.end_date - self.start_date).days
if duration > 30:
raise ValueError('Booking cannot exceed 30 days')
return self
@model_validator(mode='after')
def validate_capacity(self) -> 'Booking':
"""Validate guest count."""
if self.guests < 1:
raise ValueError('At least 1 guest required')
if self.guests > 10:
raise ValueError('Maximum 10 guests allowed')
return self
Nested Models
from pydantic import BaseModel
from typing import List
class Address(BaseModel):
"""Address component."""
street: str
city: str
state: str
zip_code: str
class Customer(BaseModel):
"""Customer with nested address."""
name: str
email: str
billing_address: Address
shipping_address: Optional[Address] = None
orders: List['Order'] = []
class Order(BaseModel):
"""Order model."""
id: str
total: Decimal
items: List['OrderItem']
class OrderItem(BaseModel):
"""Order item."""
product_id: str
quantity: int
price: Decimal
Customer.model_rebuild()
Model Configuration
from pydantic import BaseModel, ConfigDict
class StrictModel(BaseModel):
"""Model with strict validation."""
model_config = ConfigDict(
str_strip_whitespace=True,
validate_assignment=True,
validate_default=True,
strict=True,
use_enum_values=True,
populate_by_name=True,
extra='forbid',
json_schema_extra={
"example": {...}
}
)
Computed Fields
from pydantic import BaseModel, computed_field
class Rectangle(BaseModel):
width: float
height: float
@computed_field
@property
def area(self) -> float:
"""Computed area."""
return self.width * self.height
@computed_field
@property
def perimeter(self) -> float:
"""Computed perimeter."""
return 2 * (self.width + self.height)
rect = Rectangle(width=10, height=5)
print(rect.area)
print(rect.perimeter)
print(rect.model_dump())
JSON Schema
from pydantic import BaseModel
class APIResponse(BaseModel):
"""API response model."""
data: dict
message: str
status: int = 200
schema = APIResponse.model_json_schema()
@app.post("/api/endpoint")
async def endpoint(data: APIResponse) -> APIResponse:
return data
class CustomModel(BaseModel):
name: str
model_config = {
"json_schema_extra": {
"examples": [
{"name": "Example 1"},
{"name": "Example 2"}
]
}
}
Serialization
from pydantic import BaseModel, field_serializer
from datetime import datetime
class Event(BaseModel):
name: str
timestamp: datetime
metadata: dict
@field_serializer('timestamp')
def serialize_timestamp(self, value: datetime) -> str:
"""Serialize timestamp as ISO string."""
return value.isoformat()
def model_dump_json(self, **kwargs) -> str:
"""Serialize to JSON string."""
return super().model_dump_json(**kwargs)
event = Event(name="test", timestamp=datetime.now(), metadata={})
json_str = event.model_dump_json()
dict_data = event.model_dump()
❌ Anti-Patterns
def process_user(user: dict):
pass
def process_user(user: User):
pass
def validate_email(email: str):
if '@' not in email:
raise ValueError("Invalid email")
class User(BaseModel):
email: EmailStr
class Config(BaseModel):
tags: List[str] = []
class Config(BaseModel):
tags: List[str] = Field(default_factory=list)
class User(BaseModel):
middle_name: str
class User(BaseModel):
middle_name: Optional[str] = None
Best Practices Checklist
- ✅ Use
BaseModel for all data models
- ✅ Add field descriptions with
Field(..., description="...")
- ✅ Use appropriate validators for business logic
- ✅ Use
Optional for nullable fields
- ✅ Use
Field(default_factory=list) for mutable defaults
- ✅ Add JSON schema examples
- ✅ Use computed fields for derived data
- ✅ Configure strict validation when needed
- ✅ Document models with docstrings
- ✅ Use nested models for complex structures
Auto-Apply
When creating models:
- Use
BaseModel as base class
- Add type hints for all fields
- Use
Field() for constraints and descriptions
- Add validators for business logic
- Add JSON schema examples
- Use nested models for complex data
Related Skills
- structured-errors - For error response models
- docstring-format - For model documentation
- pytest-patterns - For testing models