4.4 KiB
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:
- fastapi-crud.md - CRUD endpoints using these schemas
- ../templates/pydantic-schemas.py - Schema template