Files
gh-greyhaven-ai-claude-code…/skills/api-design-standards/examples/pydantic-schemas.md
2025-11-29 18:29:15 +08:00

4.4 KiB

Pydantic Schema Examples

Complete Pydantic schema patterns for request/response validation.

Request/Response Schemas

# app/schemas/user.py
from pydantic import BaseModel, EmailStr, Field, ConfigDict, field_validator, model_validator
from datetime import datetime
from typing import Optional


class UserBase(BaseModel):
    """Shared fields for User schemas."""
    email: EmailStr
    full_name: str = Field(..., min_length=1, max_length=255)
    is_active: bool = True


class UserCreate(UserBase):
    """Schema for creating a new user."""
    password: str = Field(..., min_length=8, max_length=100)
    password_confirm: str = Field(..., min_length=8, max_length=100)

    @field_validator("password")
    @classmethod
    def validate_password_strength(cls, v: str) -> str:
        """Ensure password meets complexity requirements."""
        if len(v) < 8:
            raise ValueError("Password must be at least 8 characters")
        if not any(char.isdigit() for char in v):
            raise ValueError("Password must contain at least one digit")
        if not any(char.isupper() for char in v):
            raise ValueError("Password must contain at least one uppercase letter")
        if not any(char.islower() for char in v):
            raise ValueError("Password must contain at least one lowercase letter")
        return v

    @model_validator(mode="after")
    def passwords_match(self) -> "UserCreate":
        """Ensure password and password_confirm match."""
        if self.password != self.password_confirm:
            raise ValueError("Passwords do not match")
        return self


class UserUpdate(BaseModel):
    """Schema for updating an existing user (all fields optional)."""
    email: Optional[EmailStr] = None
    full_name: Optional[str] = Field(None, min_length=1, max_length=255)
    is_active: Optional[bool] = None


class UserRead(UserBase):
    """Schema for reading user data (public fields only)."""
    id: str
    tenant_id: str
    created_at: datetime
    updated_at: datetime

    model_config = ConfigDict(from_attributes=True)


class PaginatedResponse[T](BaseModel):
    """Generic paginated response."""
    items: list[T]
    total: int
    skip: int
    limit: int
    has_more: bool

Nested Schemas

# app/schemas/organization.py
from pydantic import BaseModel, ConfigDict, Field
from datetime import datetime
from typing import Optional
from app.schemas.team import TeamRead


class OrganizationBase(BaseModel):
    """Shared fields for Organization schemas."""
    name: str = Field(..., min_length=1, max_length=255)
    description: Optional[str] = None


class OrganizationCreate(OrganizationBase):
    """Schema for creating a new organization."""
    pass


class OrganizationRead(OrganizationBase):
    """Organization with nested teams."""
    id: str
    tenant_id: str
    teams: list[TeamRead] = []  # Nested teams
    created_at: datetime
    updated_at: datetime

    model_config = ConfigDict(from_attributes=True)

Custom Validation

# app/schemas/user.py
from pydantic import field_validator, model_validator
import re


class UserCreate(BaseModel):
    email: EmailStr
    username: str
    password: str

    @field_validator("username")
    @classmethod
    def validate_username(cls, v: str) -> str:
        """Ensure username is alphanumeric."""
        if not re.match(r"^[a-zA-Z0-9_-]+$", v):
            raise ValueError("Username must contain only letters, numbers, hyphens, and underscores")
        if len(v) < 3:
            raise ValueError("Username must be at least 3 characters")
        return v

    @field_validator("email")
    @classmethod
    def validate_email_domain(cls, v: str) -> str:
        """Ensure email is from allowed domain."""
        allowed_domains = ["example.com", "greyhaven.studio"]
        domain = v.split("@")[1]
        if domain not in allowed_domains:
            raise ValueError(f"Email must be from {', '.join(allowed_domains)}")
        return v

    @model_validator(mode="after")
    def validate_username_not_in_email(self) -> "UserCreate":
        """Ensure username is not part of email."""
        if self.username.lower() in self.email.lower():
            raise ValueError("Username cannot be part of email address")
        return self

See also: