9.6 KiB
9.6 KiB
name, description
| name | description |
|---|---|
| pydantic-models | 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."""
# Required field
name: str = Field(..., min_length=1, max_length=200)
# Optional with default
description: str = Field("", max_length=1000)
# Numeric constraints
price: Decimal = Field(..., ge=0, decimal_places=2)
quantity: int = Field(0, ge=0)
# String patterns
sku: str = Field(..., pattern=r'^[A-Z0-9-]+$')
# URL validation
image_url: Optional[HttpUrl] = None
# Email validation (requires email-validator package)
contact_email: Optional[EmailStr] = None
# Annotated types
weight: Annotated[float, Field(gt=0, description="Weight in kg")]
# Enum field
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."""
# Remove non-digits
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
# Update forward references
Customer.model_rebuild()
Model Configuration
from pydantic import BaseModel, ConfigDict
class StrictModel(BaseModel):
"""Model with strict validation."""
model_config = ConfigDict(
# Validation settings
str_strip_whitespace=True, # Strip whitespace from strings
validate_assignment=True, # Validate on field assignment
validate_default=True, # Validate default values
strict=True, # Strict type checking
# Serialization settings
use_enum_values=True, # Serialize enums as values
populate_by_name=True, # Allow field population by alias
# Extra fields
extra='forbid', # Forbid extra fields (default: 'ignore')
# JSON schema
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)
# Usage
rect = Rectangle(width=10, height=5)
print(rect.area) # 50.0
print(rect.perimeter) # 30.0
print(rect.model_dump()) # Includes computed fields
JSON Schema
from pydantic import BaseModel
class APIResponse(BaseModel):
"""API response model."""
data: dict
message: str
status: int = 200
# Generate JSON schema
schema = APIResponse.model_json_schema()
# Use in FastAPI (automatic)
@app.post("/api/endpoint")
async def endpoint(data: APIResponse) -> APIResponse:
return data
# Custom schema
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)
# Usage
event = Event(name="test", timestamp=datetime.now(), metadata={})
json_str = event.model_dump_json() # JSON string
dict_data = event.model_dump() # Python dict
❌ Anti-Patterns
# ❌ Using dict instead of model
def process_user(user: dict): # No validation!
pass
# ✅ Better: use Pydantic model
def process_user(user: User): # Validated!
pass
# ❌ Manual validation
def validate_email(email: str):
if '@' not in email:
raise ValueError("Invalid email")
# ✅ Better: use Pydantic validator
class User(BaseModel):
email: EmailStr # Built-in validation
# ❌ Mutable defaults
class Config(BaseModel):
tags: List[str] = [] # ❌ Shared across instances!
# ✅ Better: use Field with default_factory
class Config(BaseModel):
tags: List[str] = Field(default_factory=list)
# ❌ Not using Optional
class User(BaseModel):
middle_name: str # Required, but should be optional!
# ✅ Better
class User(BaseModel):
middle_name: Optional[str] = None
Best Practices Checklist
- ✅ Use
BaseModelfor all data models - ✅ Add field descriptions with
Field(..., description="...") - ✅ Use appropriate validators for business logic
- ✅ Use
Optionalfor 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
BaseModelas 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