Agent skill
py-pydantic-patterns
Pydantic v2 patterns for validation and serialization. Use when creating schemas, validating data, or working with request/response models.
Install this agent skill to your Project
npx add-skill https://github.com/aiskillstore/marketplace/tree/main/skills/cjharmath/py-pydantic-patterns
SKILL.md
Pydantic v2 Patterns
Problem Statement
Pydantic v2 has significant API changes from v1. This codebase uses v2. Wrong patterns cause validation failures, serialization bugs, and frontend integration issues.
Pattern: v1 to v2 Migration
Critical changes to know:
# ❌ v1 (OLD - don't use)
from pydantic import validator
class Model(BaseModel):
class Config:
orm_mode = True
@validator("email")
def validate_email(cls, v):
return v.lower()
def dict(self):
...
# ✅ v2 (CURRENT)
from pydantic import field_validator, ConfigDict
class Model(BaseModel):
model_config = ConfigDict(from_attributes=True)
@field_validator("email")
@classmethod
def validate_email(cls, v: str) -> str:
return v.lower()
def model_dump(self):
...
Quick reference:
| v1 | v2 |
|---|---|
class Config |
model_config = ConfigDict(...) |
orm_mode = True |
from_attributes=True |
.dict() |
.model_dump() |
.json() |
.model_dump_json() |
@validator |
@field_validator |
@root_validator |
@model_validator |
parse_obj() |
model_validate() |
update_forward_refs() |
model_rebuild() |
Pattern: Field Validators
from pydantic import BaseModel, field_validator, ValidationInfo
class AssessmentCreate(BaseModel):
title: str
skill_areas: list[str]
max_score: int
# Single field validator
@field_validator("title")
@classmethod
def title_not_empty(cls, v: str) -> str:
if not v.strip():
raise ValueError("Title cannot be empty")
return v.strip()
# Validator with access to other fields
@field_validator("max_score")
@classmethod
def validate_max_score(cls, v: int, info: ValidationInfo) -> int:
if v < 1:
raise ValueError("Max score must be positive")
return v
# Multiple fields
@field_validator("skill_areas")
@classmethod
def validate_skill_areas(cls, v: list[str]) -> list[str]:
valid = {"fundamentals", "advanced", "strategy"}
for area in v:
if area not in valid:
raise ValueError(f"Invalid skill area: {area}")
return v
Pattern: Model Validators
from pydantic import BaseModel, model_validator
class DateRange(BaseModel):
start_date: datetime
end_date: datetime
# Before validation (raw input)
@model_validator(mode="before")
@classmethod
def parse_dates(cls, data: dict) -> dict:
# Handle string dates
if isinstance(data.get("start_date"), str):
data["start_date"] = datetime.fromisoformat(data["start_date"])
return data
# After validation (validated model)
@model_validator(mode="after")
def validate_range(self) -> "DateRange":
if self.end_date < self.start_date:
raise ValueError("end_date must be after start_date")
return self
Pattern: Model Configuration
from pydantic import BaseModel, ConfigDict
class UserRead(BaseModel):
# Configure model behavior
model_config = ConfigDict(
from_attributes=True, # Allow from ORM objects
str_strip_whitespace=True, # Strip strings
str_min_length=1, # No empty strings by default
validate_default=True, # Validate default values
extra="forbid", # Error on extra fields
frozen=False, # Allow mutation
)
id: UUID
email: str
created_at: datetime
# Usage with SQLModel objects
user_db = await session.get(User, user_id)
user_read = UserRead.model_validate(user_db) # Works due to from_attributes
Pattern: Field Definitions
from pydantic import BaseModel, Field
from typing import Annotated
class AssessmentCreate(BaseModel):
# Basic constraints
title: str = Field(min_length=1, max_length=200)
score: int = Field(ge=0, le=100) # 0 <= score <= 100
rating: float = Field(gt=0, lt=5.5) # 0 < rating < 5.5
# With description (shows in OpenAPI)
skill_areas: list[str] = Field(
min_length=1,
description="List of skill areas to assess",
examples=[["fundamentals", "strategy"]],
)
# Optional with default
notes: str | None = Field(default=None, max_length=1000)
# Computed default
created_at: datetime = Field(default_factory=datetime.utcnow)
# Reusable type with constraints
PositiveInt = Annotated[int, Field(gt=0)]
Rating = Annotated[float, Field(ge=1.0, le=5.5)]
class Result(BaseModel):
count: PositiveInt
rating: Rating
Pattern: Discriminated Unions
Problem: Polymorphic responses where type depends on a field.
from pydantic import BaseModel, Field
from typing import Literal, Union
from typing_extensions import Annotated
class TextQuestion(BaseModel):
type: Literal["text"] = "text"
prompt: str
max_length: int
class MultipleChoiceQuestion(BaseModel):
type: Literal["multiple_choice"] = "multiple_choice"
prompt: str
options: list[str]
class RatingQuestion(BaseModel):
type: Literal["rating"] = "rating"
prompt: str
min_value: int
max_value: int
# Discriminated union - Pydantic uses 'type' field to determine class
Question = Annotated[
Union[TextQuestion, MultipleChoiceQuestion, RatingQuestion],
Field(discriminator="type"),
]
class Assessment(BaseModel):
questions: list[Question]
# Pydantic automatically deserializes to correct type
data = {
"questions": [
{"type": "text", "prompt": "Describe...", "max_length": 500},
{"type": "rating", "prompt": "Rate...", "min_value": 1, "max_value": 5},
]
}
assessment = Assessment.model_validate(data)
# assessment.questions[0] is TextQuestion
# assessment.questions[1] is RatingQuestion
Pattern: Custom Types
from pydantic import BaseModel, AfterValidator, BeforeValidator
from typing import Annotated
import re
# Email normalization
def normalize_email(v: str) -> str:
return v.lower().strip()
Email = Annotated[str, AfterValidator(normalize_email)]
# Phone validation
def validate_phone(v: str) -> str:
cleaned = re.sub(r"[^\d+]", "", v)
if not re.match(r"^\+?1?\d{10,14}$", cleaned):
raise ValueError("Invalid phone number")
return cleaned
PhoneNumber = Annotated[str, BeforeValidator(validate_phone)]
# UUID from string
def parse_uuid(v: str | UUID) -> UUID:
if isinstance(v, str):
return UUID(v)
return v
UUIDStr = Annotated[UUID, BeforeValidator(parse_uuid)]
class User(BaseModel):
email: Email
phone: PhoneNumber | None = None
id: UUIDStr
Pattern: Serialization Control
from pydantic import BaseModel, field_serializer, computed_field
class User(BaseModel):
id: UUID
email: str
created_at: datetime
# Custom serialization
@field_serializer("created_at")
def serialize_datetime(self, dt: datetime) -> str:
return dt.isoformat()
@field_serializer("id")
def serialize_uuid(self, id: UUID) -> str:
return str(id)
# Computed field (included in serialization)
@computed_field
@property
def display_name(self) -> str:
return self.email.split("@")[0]
# Serialization options
user.model_dump() # Full dict
user.model_dump(exclude={"created_at"}) # Exclude fields
user.model_dump(include={"id", "email"}) # Include only
user.model_dump(exclude_none=True) # Skip None values
user.model_dump(by_alias=True) # Use field aliases
user.model_dump_json() # JSON string
Pattern: Schema Inheritance
class UserBase(BaseModel):
email: str
name: str
class UserCreate(UserBase):
password: str # Only for creation
class UserRead(UserBase):
id: UUID
created_at: datetime
model_config = ConfigDict(from_attributes=True)
class UserUpdate(BaseModel):
# All optional for partial updates
email: str | None = None
name: str | None = None
password: str | None = None
Common Issues
| Issue | Likely Cause | Solution |
|---|---|---|
| "X is not a valid dict" | Using .dict() (v1) |
Use .model_dump() |
| "Unable to parse ORM object" | Missing from_attributes |
Add ConfigDict(from_attributes=True) |
| "@validator not recognized" | v1 decorator | Use @field_validator with @classmethod |
| "Extra fields not permitted" | extra="forbid" |
Remove extra fields or change config |
| Validation not running | Default value not validated | Add validate_default=True |
Detection Commands
# Find v1 patterns
grep -rn "class Config:" --include="*.py"
grep -rn "@validator" --include="*.py"
grep -rn "\.dict()" --include="*.py"
grep -rn "orm_mode" --include="*.py"
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
perigon-backend
Perigon ASP.NET Core + EF Core + Aspire conventions
perigon-agent
Pointers for Copilot/agents to apply Perigon conventions
perigon-angular
Angular 21+ standalone/Material/signal conventions for Perigon WebApp
fastapi-mastery
Comprehensive FastAPI development skill covering REST API creation, routing, request/response handling, validation, authentication, database integration, middleware, and deployment. Use when working with FastAPI projects, building APIs, implementing CRUD operations, setting up authentication/authorization, integrating databases (SQL/NoSQL), adding middleware, handling WebSockets, or deploying FastAPI applications. Triggered by requests involving .py files with FastAPI code, API endpoint creation, Pydantic models, or FastAPI-specific features.
context7-efficient
Token-efficient library documentation fetcher using Context7 MCP with 86.8% token savings through intelligent shell pipeline filtering. Fetches code examples, API references, and best practices for JavaScript, Python, Go, Rust, and other libraries. Use when users ask about library documentation, need code examples, want API usage patterns, are learning a new framework, need syntax reference, or troubleshooting with library-specific information. Triggers include questions like "Show me React hooks", "How do I use Prisma", "What's the Next.js routing syntax", or any request for library/framework documentation.
browser-use
Browser automation using Playwright MCP. Navigate websites, fill forms, click elements, take screenshots, and extract data. Use when tasks require web browsing, form submission, web scraping, UI testing, or any browser interaction.
Didn't find tool you were looking for?