mcpField
P
pydantic-v2

pydantic-v2

github.com/bm629Development6117 installsUpdated 1w ago
Repository

About pydantic-v2

A reference skill for writing current Pydantic v2 code, aimed at developers defining and validating Python data models. It focuses on v2 APIs and common migration pitfalls.

Key features

  1. 01Typed models and field constraints
  2. 02Field and model validators
  3. 03Serialization and computed fields
  4. 04Settings from environment and .env
  5. 05TypeAdapter and union validation
  6. 06v1-to-v2 migration guidance

Use cases

  1. 01Define validated DTOs and configuration models
  2. 02Add custom field or cross-field validation
  3. 03Serialize models for API or data exchange
  4. 04Load application settings from environment variables

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/bm629/agent-skills --skill pydantic-v2

After installing, type/pydantic-v2in Claude Code to confirm it loaded.

Bundled files

References and scripts installed along with SKILL.md.

references/
migration-v1-to-v2.md
serialization.md
settings.md
sources.md
validators.md

Details

From the project README

Pydantic v2

Overview

Pydantic v2 validates and serializes data from Python type hints. A BaseModel subclass declares fields as annotations; constructing it validates and coerces the input, and the instance round-trips to dicts/JSON via model_dump*. This skill is the knowledge delta for writing current v2 — the post-v1 API (model_config = ConfigDict(...), model_dump(), @field_validator) plus the pitfalls that bite when v1 muscle memory leaks in. It covers standalone Pydantic: models, fields, validators, serialization, config, pydantic-settings, TypeAdapter, and structured/union types. For framework-level request/response modeling and response_model, see the fastapi skill — this skill does not duplicate framework mechanics.

When to activate

  • ✅ Defining a data model, config object, or DTO with typed fields + constraints.
  • ✅ Writing field- or model-level validation, custom serialization, or computed fields.
  • ✅ Loading app configuration from env vars / .env with pydantic-settings.
  • ✅ Validating non-model shapes in bulk (list[Model], dicts, primitives) with TypeAdapter.
  • ✅ Modernizing v1-era code (class Config, .dict(), @validator) to v2.

Do NOT activate when:

  • The task is FastAPI request/response wiring or response_model — use the fastapi skill.
  • The topic is Pydantic-AI agents or Logfire instrumentation — different products, out of scope.

Workflow

Step 1: Define the model with type hints

A field is name: Type. With no default it is required; a default makes it optional. Critically in v2, Optional[int] (i.e. int | None) only marks the field nullable — it does NOT supply a default; omit the default and the field is still required. Use default_factory for mutable defaults (never a literal []/{}, which is shared across instances).

from pydantic import BaseModel, Field

class User(BaseModel):
    id: int                                   # required
    name: str = "anon"                        # optional, has default
    nickname: str | None = None               # nullable AND optional (explicit default)
    tags: list[str] = Field(default_factory=list)  # safe mutable default

Construct from trusted kwargs (User(id=1)) or validate untrusted input with the classmethods User.model_validate(obj) (dict / object) and User.model_validate_json(data) (JSON str/bytes). model_construct(**data) skips validation — trusted data only.

Step 2: Constrain fields with Annotated[..., Field(...)]

Prefer the Annotated style; it keeps the type honest and composes cleanly. Field supplies gt/ge/lt/le/multiple_of (numeric), min_length/max_length/pattern (string), plus default, default_factory, description, alias, and exclude.

from typing import Annotated
from pydantic import BaseModel, Field

class Account(BaseModel):
    balance: Annotated[int, Field(ge=0)]
    handle: Annotated[str, Field(min_length=3, max_length=32, pattern=r"^[a-z0-9_]+$")]
    score: Annotated[float, Field(default=0.0, le=100.0)]
Step 3: Validate with @field_validator / @model_validator

Field validators run per-field; model validators run across fields. See references/validators.md for before/after/ plain/wrap modes, ValidationInfo, and the @classmethod rule.

from typing_extensions import Self
from pydantic import BaseModel, field_validator, model_validator

class Signup(BaseModel):
    email: str
    password: str
    password_repeat: str

    @field_validator("email", mode="after")
    @classmethod
    def lowercase(cls, v: str) -> str:
        return v.lower()

    @model_validator(mode="after")
    def passwords_match(self) -> Self:
        if self.password != self.password_repeat:
            raise ValueError("passwords do not match")
        return self
Step 4: Serialize with model_dump*

model_dump() → dict, model_dump_json() → JSON string. Shape with include / exclude / exclude_unset / exclude_defaults / exclude_none / by_alias / mode. Customize with @field_serializer and add derived output with @computed_field. See references/serialization.md.

user.model_dump()                              # dict, python types
user.model_dump(exclude={"password"})          # drop a field
user.model_dump(exclude_unset=True)            # only explicitly-set fields (PATCH-style)
user.model_dump_json(by_alias=True, exclude_none=True, indent=2)
Step 5: Configure with model_config = ConfigDict(...)

The class attribute model_config (NOT a nested class Config) holds a ConfigDict. Common options: strict, from_attributes (read ORM objects), validate_assignment, use_enum_values, extra ("ignore"/"allow"/ "forbid"), frozen, str_strip_whitespace. For alias-by-name, current 2.11+ prefers validate_by_name=True (with validate_by_alias=True) — see Gotchas.

from pydantic import BaseModel, ConfigDict

class Row(BaseModel):
    model_config = ConfigDict(from_attributes=True, extra="forbid", frozen=True)
    id: int
    label: str

obj = Row.model_validate(orm_row)   # from_attributes reads attributes off an ORM object
Step 6: Load config with pydantic-settings

BaseSettings lives in the separate pydantic-settings package (not core pydantic). It reads fields from environment variables and .env. Configure via SettingsConfigDict. See references/settings.md for nested settings and source precedence.

from pydantic_settings import BaseSettings, SettingsConfigDict

class Settings(BaseSettings):
    model_config = SettingsConfigDict(env_prefix="APP_", env_file=".env",
                                      env_file_encoding="utf-8")
    api_key: str                  # required → from APP_API_KEY
    host: str = "127.0.0.1"
    port: int = 8000              # coerced from the string env value

settings = Settings()             # reads env / .env at construction
Step 7: Structure — enums, Literal, unions, TypeAdapter

Nest models by using one as another's field type. Use Enum (subclass str, Enum for JSON-friendly values) or Literal for fixed choices. Discriminated unions dispatch on a tag field via Field(discriminator=...). Validate non-model shapes (lists, dicts, primitives) with TypeAdapter — it is the v2-idiomatic choice over RootModel for bulk/standalone validation.

from typing import Annotated, Literal
from pydantic import BaseModel, Field, TypeAdapter

class Cat(BaseModel):
    kind: Literal["cat"]
    lives: int

class Dog(BaseModel):
    kind: Literal["dog"]
    good_boy: bool = True

class Owner(BaseModel):
    pet: Annotated[Cat | Dog, Field(discriminator="kind")]

cats = TypeAdapter(list[Cat]).validate_python([{"kind": "cat", "lives": 9}])
Step 8: Handle ValidationError

model_validate* and construction raise ValidationError on bad input. .errors() returns a list of dicts (type, loc, msg, input, url); .error_count() returns the count; .json() serializes them.

from pydantic import ValidationError

try:
    User.model_validate({"id": "not-an-int"})
except ValidationError as e:
    for err in e.errors():
        print(".".join(map(str, err["loc"])), err["type"], err["msg"])

Rules

Hard rules (never violate):

  • v2 API only. model_config = ConfigDict(...), model_dump() / model_dump_json(), model_validate() / model_validate_json(), @field_validator / @model_validator, model_json_schema(). Never class Config, .dict() / .json(), @validator / @root_validator, .parse_obj() / .parse_raw(), or .schema().
  • @field_validator must have @classmethod directly beneath it. @model_validator(mode="after") operates on self and does NOT.
  • Optional[x] is not a default. Provide one explicitly (x | None = None) if the field should be optional.
  • Mutable defaults via default_factory, never a literal [] / {} / dict().
  • BaseSettings imports from pydantic_settings, not pydantic.
  • Don't use TypeAdapter as a field annotation inside a BaseModel; use it standalone for validating non-model types.

Preferences (override-able):

  • Prefer Annotated[T, Field(...)] over assigning Field(...) as the default value.
  • Separate request and response models rather than reusing one model for both.
  • Reuse a TypeAdapter instance (build once, validate many) for hot paths.

Gotchas

  • populate_by_name is on the way out. In Pydantic 2.11+, prefer ConfigDict(validate_by_name=True, validate_by_alias=True). populate_by_name still works in 2.x (pending deprecation in v3) — fine on older 2.x, but write the new pair on current versions.
  • class Config silently still works but is deprecated — it won't error, so v1 code "looks fine" yet is wrong style and loses v2 options. Convert to model_config = ConfigDict(...).
  • Forgetting @classmethod on a @field_validator raises at class-definition time or misbehaves — the decorator order is @field_validator(...) on top, @classmethod directly under it.
  • model_config as a plain dict works but loses type-checker help — use the ConfigDict(...) constructor so IDEs and ty/mypy validate the keys.
  • Lax coercion surprises. By default "123" coerces to 123. If you need exactness, set ConfigDict(strict=True) (or Field(strict=True) per field).
  • exclude_unset vs exclude_defaults. exclude_unset drops fields the caller never set (ideal for PATCH bodies); exclude_defaults drops fields equal to their default even if set. They are not interchangeable.

Anti-patterns

  • "It's just a small model, I'll use the old .dict()." No — .dict() is removed-in-spirit/deprecated; use model_dump(). Mixed v1/v2 idioms are the top source of broken Pydantic code.
  • Reaching for @root_validator because you "remember it" — it does not exist in v2; use @model_validator(mode="before"|"after").
  • Wrapping a bare type in a one-field RootModel to validate a list/dict — use TypeAdapter(list[T]) instead.
  • Re-implementing FastAPI request/response handling here — that belongs to the fastapi skill; this skill defines the models, the framework consumes them.

Output

Correct, current Pydantic v2 source: model classes with ConfigDict, typed + constrained fields, @field_validator/@model_validator validation, model_dump* serialization, pydantic-settings config classes, and TypeAdapter/discriminated-union structures — free of any v1-era idiom. The consumer is whatever code or workflow phase needs validated, serializable data models; when those models back an HTTP API, the fastapi skill governs the framework wiring.

Related

  • fastapi — framework specifics: request/response models, response_model, dependency-injected validation. This skill defines the models; fastapi consumes them. Do not duplicate framework mechanics here.
  • Official docs (provenance): see references/sources.md.

Progressive disclosure

Heavy detail lives in references/, loaded on demand:

  • references/validators.md — load when writing validators: all @field_validator/@model_validator modes, ValidationInfo, the @classmethod rule, before/after ordering.
  • references/serialization.md — load when customizing output: every model_dump* kwarg, @field_serializer, @model_serializer, @computed_field, alias kinds (validation_alias / serialization_alias / AliasChoices).
  • references/settings.md — load for pydantic-settings depth: nested settings, env_nested_delimiter, source precedence, secrets_dir.
  • references/migration-v1-to-v2.md — load when modernizing v1 code: the full rename table and behavior changes.
  • references/sources.md — research provenance.

Body budget

  • description ≤ 1,024 chars (respected).
  • Body ~290 lines / well under the 5,000-token soft target.
  • Reference files each well under the 10k-token warn threshold.

Comments

Markdown supported