--- 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 ```python 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 ```python 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 ```python 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 ```python 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 ```python 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 ```python 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 ```python 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 ```python 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 ```python 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 ```python # ❌ 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 `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: 1. Use `BaseModel` as base class 2. Add type hints for all fields 3. Use `Field()` for constraints and descriptions 4. Add validators for business logic 5. Add JSON schema examples 6. Use nested models for complex data ## Related Skills - structured-errors - For error response models - docstring-format - For model documentation - pytest-patterns - For testing models