skills/cjharmath/py-pydantic-patterns/SKILL.md
Pydantic v2 patterns for validation and serialization. Use when creating schemas, validating data, or working with request/response models.
npx skillsauth add aiskillstore/marketplace py-pydantic-patternsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Pydantic v2 has significant API changes from v1. This codebase uses v2. Wrong patterns cause validation failures, serialization bugs, and frontend integration issues.
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() |
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
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
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
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
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
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
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
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
| 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 |
# 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"
development
Apple Human Interface Guidelines for content display components. Use this skill when the user asks about charts component, collection view, image view, web view, color well, image well, activity view, lockup, data visualization, content display, displaying images, rendering web content, color pickers, or presenting collections of items in Apple apps. Also use when the user says how should I display charts, what's the best way to show images, should I use a web view, how do I build a grid of items, what component shows media, or how do I present a share sheet. Cross-references: hig-foundations for color/typography/accessibility, hig-patterns for data visualization patterns, hig-components-layout for structural containers, hig-platforms for platform-specific component behavior.
tools
Automate HelpDesk tasks via Rube MCP (Composio): list tickets, manage views, use canned responses, and configure custom fields. Always search tools first for current schemas.
testing
Expert Haskell engineer specializing in advanced type systems, pure functional design, and high-reliability software. Use PROACTIVELY for type-level programming, concurrency, and architecture guidance.
tools
GraphQL gives clients exactly the data they need - no more, no less. One endpoint, typed schema, introspection. But the flexibility that makes it powerful also makes it dangerous. Without proper controls, clients can craft queries that bring down your server. This skill covers schema design, resolvers, DataLoader for N+1 prevention, federation for microservices, and client integration with Apollo/urql. Key insight: GraphQL is a contract. The schema is the API documentation. Design it carefully.