Files
2025-11-30 08:51:46 +08:00

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 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
  • structured-errors - For error response models
  • docstring-format - For model documentation
  • pytest-patterns - For testing models