AgentStack
SKILL verified MIT Self-run

Pydantic

skill-kid-sid-claude-spellbook-pydantic · by kid-sid

Use when defining request/response schemas, writing custom validators, controlling serialization for PATCH endpoints, validating non-model data with TypeAdapter, or configuring app settings from environment variables with pydantic-settings.

No reviews yet
0 installs
16 views
0.0% view→install

Install

$ agentstack add skill-kid-sid-claude-spellbook-pydantic

✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.

Security review

✓ Passed

No issues found. Passed automated security review. · v0.1.0 How review works →

  • Prompt-injection patterns
  • Secret / credential exfiltration
  • Dangerous shell & filesystem operations
  • Untrusted network calls
  • Known-malicious package signatures

What it can access

  • Network access No
  • Filesystem access No
  • Shell / process execution No
  • Environment & secrets Used
  • Dynamic code execution No

From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.

Are you the author of Pydantic? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

About

Pydantic v2 Patterns

Validation, serialization, and settings management with Pydantic v2.

When to Activate

  • Defining request/response schemas or domain models
  • Writing @field_validator or @model_validator for custom validation
  • Using Annotated to build reusable constrained types
  • Controlling serialization with model_dump() / model_dump_json()
  • Building generic models or discriminated unions
  • Validating arbitrary data (not a model) with TypeAdapter
  • Configuring app settings from environment variables with pydantic-settings

BaseModel Basics

from pydantic import BaseModel, Field
from datetime import datetime
from uuid import UUID

class User(BaseModel):
    id: UUID
    name: str
    email: str
    age: int = Field(ge=0, le=150)
    role: str = "user"          # default value
    created_at: datetime | None = None

# Instantiate
user = User(id="a1b2...", name="Alice", email="alice@example.com", age=30)

# Access
user.name        # "Alice"
user.model_fields  # dict of FieldInfo

# Validate from dict / JSON
user = User.model_validate({"id": "...", "name": "Alice", ...})
user = User.model_validate_json('{"id": "...", "name": "Alice", ...}')

Field Constraints

from pydantic import BaseModel, Field
from typing import Annotated

class Product(BaseModel):
    name: str = Field(min_length=1, max_length=200, strip_whitespace=True)
    price: float = Field(gt=0, description="Price in USD")
    discount: float = Field(ge=0, le=1, default=0.0)    # 0–100%
    tags: list[str] = Field(default_factory=list, max_length=10)
    sku: str = Field(pattern=r"^[A-Z]{3}-\d{6}$")
    metadata: dict = Field(default_factory=dict)

    # Alias — accept "product_name" in input, use "name" in Python
    name: str = Field(alias="product_name")

Reusable constrained types with Annotated

from typing import Annotated
from pydantic import Field

# Define once, reuse everywhere
PositiveInt   = Annotated[int,   Field(gt=0)]
Percentage    = Annotated[float, Field(ge=0.0, le=1.0)]
NonEmptyStr   = Annotated[str,   Field(min_length=1, strip_whitespace=True)]
EmailStr      = Annotated[str,   Field(pattern=r"^[^@]+@[^@]+\.[^@]+$")]
UserId        = Annotated[str,   Field(min_length=36, max_length=36)]

class CreateUserRequest(BaseModel):
    name: NonEmptyStr
    email: EmailStr
    age: PositiveInt
    discount: Percentage = 0.0

Validators

@field_validator — validate / transform a single field

from pydantic import BaseModel, field_validator

class User(BaseModel):
    name: str
    email: str
    role: str

    @field_validator("email")
    @classmethod
    def lowercase_email(cls, v: str) -> str:
        return v.strip().lower()

    @field_validator("role")
    @classmethod
    def valid_role(cls, v: str) -> str:
        allowed = {"admin", "user", "viewer"}
        if v not in allowed:
            raise ValueError(f"role must be one of {allowed}")
        return v

    # Validate multiple fields at once
    @field_validator("name", "email", mode="before")  # runs before type coercion
    @classmethod
    def strip_strings(cls, v: str) -> str:
        return v.strip() if isinstance(v, str) else v

mode="before" runs before type coercion. mode="after" (default) runs after.

@model_validator — validate across multiple fields

from pydantic import BaseModel, model_validator

class DateRange(BaseModel):
    start_date: datetime
    end_date: datetime
    max_days: int = 90

    @model_validator(mode="after")
    def check_date_range(self) -> "DateRange":
        if self.end_date  self.max_days:
            raise ValueError(f"Range cannot exceed {self.max_days} days")
        return self

class PasswordReset(BaseModel):
    password: str
    confirm_password: str

    @model_validator(mode="after")
    def passwords_match(self) -> "PasswordReset":
        if self.password != self.confirm_password:
            raise ValueError("Passwords do not match")
        return self

# mode="before" — receives raw dict, before field validation
    @model_validator(mode="before")
    @classmethod
    def handle_legacy_format(cls, data: dict) -> dict:
        if "user_name" in data:
            data["name"] = data.pop("user_name")   # rename legacy field
        return data

ConfigDict

from pydantic import BaseModel, ConfigDict

class UserResponse(BaseModel):
    model_config = ConfigDict(
        from_attributes=True,       # allow ORM model → Pydantic (was orm_mode in v1)
        populate_by_name=True,      # accept both alias and field name
        str_strip_whitespace=True,  # strip whitespace from all str fields
        str_to_lower=False,
        extra="forbid",             # reject unknown fields (good for request schemas)
        # extra="ignore"            # silently drop unknown fields
        # extra="allow"             # keep unknown fields in __pydantic_extra__
        frozen=True,                # immutable instances (hashable)
        arbitrary_types_allowed=True,  # allow non-Pydantic types
        json_schema_extra={"example": {"name": "Alice", "email": "alice@example.com"}},
    )

Serialization

user = User(id=uuid4(), name="Alice", email="alice@example.com", role="admin")

# To dict
user.model_dump()
user.model_dump(exclude={"password", "internal_id"})
user.model_dump(include={"id", "name", "email"})
user.model_dump(exclude_none=True)      # omit None values
user.model_dump(exclude_unset=True)     # omit fields not explicitly set (useful for PATCH)
user.model_dump(by_alias=True)          # use field aliases as keys
user.model_dump(mode="json")            # serialize to JSON-compatible types (UUID → str)

# To JSON string
user.model_dump_json()
user.model_dump_json(indent=2, exclude_none=True)

# From ORM (with from_attributes=True)
orm_user = db.query(UserORM).first()
user = UserResponse.model_validate(orm_user)

# Copy with overrides
updated = user.model_copy(update={"role": "admin"})

Discriminated Unions

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

class CreditCard(BaseModel):
    type: Literal["credit_card"]
    number: str
    expiry: str
    cvv: str

class BankTransfer(BaseModel):
    type: Literal["bank_transfer"]
    account_number: str
    routing_number: str

class Crypto(BaseModel):
    type: Literal["crypto"]
    wallet_address: str
    currency: str

PaymentMethod = Annotated[
    Union[CreditCard, BankTransfer, Crypto],
    Field(discriminator="type"),   # Pydantic uses "type" to pick the right model
]

class Order(BaseModel):
    id: str
    payment: PaymentMethod

# Pydantic automatically picks the right union member
order = Order.model_validate({
    "id": "o-123",
    "payment": {"type": "credit_card", "number": "4111...", "expiry": "12/26", "cvv": "123"},
})
isinstance(order.payment, CreditCard)  # True

Generic Models

from pydantic import BaseModel
from typing import TypeVar, Generic

T = TypeVar("T")

class Page(BaseModel, Generic[T]):
    items: list[T]
    total: int
    page: int
    page_size: int
    has_next: bool

class ApiResponse(BaseModel, Generic[T]):
    data: T
    status: int = 200
    message: str = "ok"

# Concrete usage — fully typed
users_page: Page[User] = Page[User](items=[...], total=100, page=1, page_size=20, has_next=True)
response: ApiResponse[User] = ApiResponse[User](data=user)

TypeAdapter — validate without a model

from pydantic import TypeAdapter

# Validate a plain type or complex type
ta = TypeAdapter(list[int])
ta.validate_python([1, 2, "3"])   # [1, 2, 3] — coerces "3" to 3
ta.validate_json("[1, 2, 3]")

# Validate arbitrary dict shape
ta = TypeAdapter(dict[str, list[int]])
ta.validate_python({"a": [1, 2], "b": [3]})

# Great for validating webhook payloads, external API responses
StrippedStr = Annotated[str, Field(strip_whitespace=True, min_length=1)]
ta = TypeAdapter(StrippedStr)
ta.validate_python("  hello  ")   # "hello"

pydantic-settings

from pydantic_settings import BaseSettings, SettingsConfigDict
from pydantic import Field
from functools import lru_cache

class Settings(BaseSettings):
    model_config = SettingsConfigDict(
        env_file=".env",
        env_file_encoding="utf-8",
        case_sensitive=False,
        extra="ignore",
    )

    # Required — raises if missing from env
    database_url: str
    secret_key: str

    # Optional with defaults
    environment: str = "development"
    debug: bool = False
    redis_url: str = "redis://localhost:6379"
    allowed_origins: list[str] = ["http://localhost:3000"]

    # Nested prefix: reads TEMPORAL__ADDRESS from env
    temporal_address: str = Field("localhost:7233", alias="TEMPORAL_ADDRESS")

    @property
    def is_production(self) -> bool:
        return self.environment == "production"

@lru_cache
def get_settings() -> Settings:
    return Settings()

settings = get_settings()   # cached singleton

Env var names match field names case-insensitively. list[str] reads from ALLOWED_ORIGINS=http://a.com,http://b.com (comma-separated).


Red Flags

  • Sharing API schemas with the domain layer — using the same Pydantic model as both the HTTP request schema and the internal domain entity couples the API contract to business logic; changes to the API surface silently affect domain behavior and vice versa
  • Mutable field defaults without default_factorytags: list[str] = [] shares the same list object across all instances; use tags: list[str] = Field(default_factory=list) for any mutable default
  • Not using model_dump(exclude_unset=True) for PATCHmodel_dump() on a partial-update model includes all fields set to their defaults, overwriting database values the client never sent; exclude_unset=True returns only the fields the caller explicitly provided
  • orm_mode = True (v1 syntax) in a v2 project — the v1 config key is silently ignored in Pydantic v2; use model_config = ConfigDict(from_attributes=True) instead
  • Catching bare Exception from model_validate — validation errors from Pydantic are ValidationError, not ValueError or Exception; catching the wrong type means bad input crashes the caller with an unhandled exception instead of a structured error response
  • model_dump() when JSON-safe types are neededmodel_dump() returns Python objects (UUID, datetime, Decimal) that are not JSON-serializable; use model_dump(mode="json") or model_dump_json() when the result will be serialized to JSON or stored as a dict in MongoDB
  • Repeating Field(gt=0) on every model instead of Annotated types — duplicating constraints is error-prone and hard to update; define PositiveInt = Annotated[int, Field(gt=0)] once and reuse it everywhere

Checklist

  • [ ] Annotated used to define reusable constrained types (not repeating Field() everywhere)
  • [ ] @field_validator with mode="before" for input normalization (strip, lowercase)
  • [ ] @model_validator for cross-field validation (date ranges, password confirm)
  • [ ] from_attributes=True in ConfigDict for ORM → schema conversion
  • [ ] extra="forbid" on request schemas to reject unknown input
  • [ ] model_dump(exclude_unset=True) for PATCH endpoints (only update what was sent)
  • [ ] model_dump(mode="json") when serializing UUIDs/datetimes to dicts
  • [ ] TypeAdapter for validating non-model types (lists, dicts, scalars)
  • [ ] pydantic-settings for all environment variable config (not raw os.environ)
  • [ ] @lru_cache on get_settings() — load once, reuse

Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet — be the first.

Versions

  • v0.1.0 Imported from the upstream source.