frontier-python-ts/skills/pydantic/SKILL.md
Pydantic v2 patterns for request/response schemas, validators, and data conversion in FastAPI services. Load when defining BaseModel classes, designing API schemas, writing field validators, or converting between SQLAlchemy models and Pydantic schemas. Use alongside `fastapi` and `pydantic-settings` (the latter handles env vars specifically).
npx skillsauth add jon23d/skillz pydanticInstall 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 is the validation and serialisation layer for every backend service. The split is rigid: SQLAlchemy models live in app/models/, Pydantic schemas live in app/schemas/, and the two never import each other.
When in doubt: the schema is the contract; the model is the storage.
# app/schemas/user.py
from datetime import datetime
from pydantic import BaseModel, EmailStr, Field, ConfigDict
class UserBase(BaseModel):
email: EmailStr
name: str = Field(min_length=1, max_length=200)
class UserCreate(UserBase):
password: str = Field(min_length=12, max_length=200)
class UserUpdate(BaseModel):
name: str | None = Field(default=None, min_length=1, max_length=200)
email: EmailStr | None = None
class UserRead(UserBase):
id: str
tenant_id: str
created_at: datetime
updated_at: datetime
model_config = ConfigDict(from_attributes=True)
Rules:
Create, Update, Read. Compose them via inheritance from a Base if they share fields.Update schemas use | None defaults so PATCH semantics work — only set fields are updated.Read schemas use from_attributes=True so FastAPI's response_model can construct them directly from a SQLAlchemy row.Read schemas. Ever. The fact that this is even possible is why Read and Create exist as separate types.Use Field(...) for everything the database also constrains. The schema is the first layer of defence; the DB is the second.
from pydantic import BaseModel, Field, HttpUrl, conint, constr
class ProjectCreate(BaseModel):
name: str = Field(min_length=1, max_length=200)
description: str | None = Field(default=None, max_length=10_000)
homepage: HttpUrl | None = None
priority: int = Field(ge=0, le=100)
Use the dedicated types (EmailStr, HttpUrl, IPvAnyAddress) instead of regex strings. They are tested and the failure messages are clear.
Use field_validator for single-field rules and model_validator for cross-field rules.
from pydantic import BaseModel, field_validator, model_validator
class DateRange(BaseModel):
start: datetime
end: datetime
@field_validator("start", "end")
@classmethod
def must_be_utc(cls, v: datetime) -> datetime:
if v.tzinfo is None:
raise ValueError("must be timezone-aware")
return v.astimezone(timezone.utc)
@model_validator(mode="after")
def end_after_start(self) -> "DateRange":
if self.end <= self.start:
raise ValueError("end must be after start")
return self
Rules:
mode="after" for cross-field validation — runs after individual fields are coerced.ValueError. Pydantic converts it to a ValidationError with the right field path. Do not raise HTTPException — that is the route's job.Use them for polymorphic payloads instead of optional fields.
from typing import Annotated, Literal
from pydantic import BaseModel, Field
class EmailNotification(BaseModel):
kind: Literal["email"] = "email"
to: EmailStr
subject: str
class SmsNotification(BaseModel):
kind: Literal["sms"] = "sms"
to: str
body: str
Notification = Annotated[
EmailNotification | SmsNotification,
Field(discriminator="kind"),
]
The OpenAPI spec generated by FastAPI will document these as a proper oneOf with a discriminator, and the frontend's generated types will be a tagged union — no casts.
response_model=UserRead on the route and return the SQLAlchemy row. from_attributes=True makes this work.**payload.model_dump() blindly. Blind unpacking lets new schema fields silently set unintended model attributes.# Good
user = User(
tenant_id=current_user.tenant_id,
email=payload.email,
name=payload.name,
password_hash=hash_password(payload.password),
)
# Bad — fragile, leaks fields
user = User(**payload.model_dump())
model_dump() for plain dicts (e.g. logging).model_dump_json() for JSON strings (e.g. cache values, queue payloads).exclude_none=True when sending to APIs that distinguish "absent" from "null".dict(model) — it does not recurse into nested models.from_attributes=True on Create schemas — pointless. Only Read schemas need it.HTTPException from a validator — wrong layer. Raise ValueError.Create and Read — leads to leaking fields. Always split.BaseSettings from pydantic — moved to pydantic-settings package in v2. See the pydantic-settings skill.**model.model_dump() to construct an ORM row — fragile. Map fields explicitly.development
Use when adding or modifying environment variable handling in TypeScript projects or monorepos — especially when using process.env directly, missing startup validation, sharing env schemas across packages, or encountering "undefined is not a string" errors at runtime from missing env vars.
testing
Use when creating a new skill, editing an existing skill, writing a SKILL.md, or verifying a skill works before deployment.
development
React UI design principles and conventions. Load when building or modifying any user interface or React components. Covers application type detection, visual standards, component design and structure, Mantine (business apps) and Tailwind (consumer apps), accessibility, responsiveness, state management, data fetching, testing, and in-app help patterns.
development
Use when setting up ESLint and/or Prettier in a TypeScript project, adding linting to an existing TypeScript codebase, or configuring typescript-eslint, eslint-config-prettier, or related packages.