From pydantic-consistency
Write, review, refactor, or debug Python code that uses Pydantic (BaseModel, Field, field_validator, model_validator, ConfigDict, TypeAdapter, BaseSettings) using one canonical, modern v2 idiom set. Use this skill whenever code defines or validates Pydantic models, serializes them (model_dump, model_dump_json), migrates off v1 APIs (@validator, @root_validator, .dict(), .json(), parse_obj, class Config), handles ValidationError, builds FastAPI request/response models, or when the user asks "why is my validator not running," "why does Optional not default to None anymore," or shows a PydanticUserError / PydanticDeprecatedSince20 warning. Trigger it even when the user just says "make a model for this payload" or "validate this config" — without saying the word "pydantic v2."
How this skill is triggered — by the user, by Claude, or both
Slash command
/pydantic-consistency:pydantic-consistencyThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Pydantic is stable and extremely well represented in training data — which is the problem:
Pydantic is stable and extremely well represented in training data — which is the problem:
half of that data is v1. Generated code routinely mixes v1 decorators (@validator,
@root_validator), v1 methods (.dict(), .json(), parse_obj), and v1 class Config
with v2 names, producing deprecation warnings at best and validators that silently never run
at worst. This skill pins one canonical idiom set — Pydantic v2 — so every model you
write or review follows the same rules instead of mixing eras.
| Always | Never | Why |
|---|---|---|
@field_validator("name", mode="before"/"after") | @validator("name", pre=True) | v1 decorator; deprecated in v2, different signature (info: ValidationInfo, no values dict). |
@model_validator(mode="before"/"after") | @root_validator(...) | v1 decorator; mode="after" receives self, not a values dict. |
model.model_dump() / model.model_dump_json() | model.dict() / model.json() | v1 methods; deprecated, miss v2-only options (mode="json", by_alias, exclude_none). |
Model.model_validate(obj) / model_validate_json(s) | Model.parse_obj(obj) / parse_raw(s) | v1 constructors; deprecated. |
model_config = ConfigDict(...) | class Config: inner class | v1 config style; deprecated, and v1 key names (allow_population_by_field_name) don't exist in v2. |
name: Annotated[str, Field(min_length=1)] | mixing bare Field(...) defaults and Annotated styles in one codebase | Pick one; Annotated is the house style and composes with type aliases. |
tags: list[str] = Field(default_factory=list) | tags: list[str] = [] | Pydantic copies mutable defaults, but default_factory is explicit and survives strict linting; never rely on it for non-Pydantic dataclasses. |
middle: str | None = None (explicit default) | middle: Optional[str] with no = | In v2 Optional no longer implies a default — the field is required and None is just allowed. |
from pydantic_settings import BaseSettings | from pydantic import BaseSettings | Moved to the separate pydantic-settings package in v2; the old import raises. |
@field_serializer("when") / @model_serializer | class Config: json_encoders = {...} | json_encoders is deprecated v1 machinery. |
TypeAdapter(list[Item]).validate_python(data) | wrapping lists in a throwaway __root__ model | __root__ is gone in v2; TypeAdapter (or RootModel) is the replacement. |
Field(discriminator="kind") tagged unions | plain Union[...] for tagged payloads | Discriminated unions give exact errors and O(1) dispatch instead of try-each-member. |
House style for a model:
from typing import Annotated, Literal
from pydantic import BaseModel, ConfigDict, Field, field_validator
class User(BaseModel):
model_config = ConfigDict(frozen=True, extra="forbid")
id: Annotated[int, Field(gt=0)]
email: Annotated[str, Field(min_length=3)]
role: Literal["admin", "member"] = "member"
nickname: str | None = None
tags: list[str] = Field(default_factory=list)
@field_validator("email")
@classmethod
def email_has_at(cls, v: str) -> str:
if "@" not in v:
raise ValueError("email must contain @")
return v.lower()
Optional[str] without = None makes the field required. Code ported from v1
starts raising "Field required" — or worse, someone "fixes" it by feeding None
explicitly everywhere. Always write the default.pre=True vs mode="before", each_item is gone — validate the container or
use Annotated item types). Migrate fully, never half.model_dump() vs model_dump(mode="json"): the default mode="python" keeps
datetime/UUID/Decimal objects. If the dict is headed for json.dumps or a JSON
column, use mode="json" (or model_dump_json()), or serialization fails downstream."42" validates as int, 1 as bool in some paths.
At trust boundaries where that matters, use ConfigDict(strict=True) or
Annotated[int, Field(strict=True)] deliberately — and document the choice.extra defaults to "ignore": typo'd input keys vanish silently. API request models
generally want extra="forbid".@computed_field (appears in model_dump) — not by
mutating in a validator on a frozen model or recomputing at call sites.ValueError instead of ValidationError: handle
pydantic.ValidationError and report e.errors() (each item has loc, msg, type)
so multi-field failures surface as a list, not just the first message.BaseModel for everything: for plain internal data holders use
dataclasses.dataclass (validate at the boundary with TypeAdapter(MyDataclass) if
needed). Models everywhere means validation cost everywhere and mutation-by-default
nowhere.Target Pydantic v2 (2.x). The breaking line is 2.0: validator decorators renamed,
methods renamed to model_*, class Config → ConfigDict, BaseSettings extracted to
pydantic-settings, __root__ removed, Optional no longer implies a default. The
pydantic.v1 compatibility namespace exists for incremental migration only — never import
from it in new code, and never mix pydantic.v1 and v2 models in one inheritance chain.
pydantic>=2 in requirements). If genuinely pinned to v1, say so and
write pure v1 — never era-mixed code.Annotated fields, explicit defaults (= None for optionals),
default_factory for mutables, and model_config = ConfigDict(...).Model.model_validate(...) / model_validate_json(...);
TypeAdapter for non-model shapes; discriminated unions for tagged data.model_dump(mode="json") when JSON-bound; field_serializer
for custom representations.For the fuller migration map (v1 API → v2 API), expanded gotcha explanations, and more
worked examples, read references/pydantic-patterns.md.
Creates, edits, and optimizes skills for Claude Code, including drafting, evaluating with test prompts, iterating on performance, and improving skill descriptions for better triggering accuracy.
npx claudepluginhub guidogl/pydantic-consistency --plugin pydantic-consistency