Community/Tools

Pydantic v2: validate data at the edges of your Python application

Pydantic turns type hints into fast runtime validation and serialisation. Here are the core patterns for API payloads, settings and LLM outputs, plus the v2 changes that trip people up.

Author's connection to this tool: No connection. An independent overview written by the RecallRun editors.

Most bugs that reach production come in through the edges of a system: an API request with a missing field, a config value that's a string instead of a number, a third-party webhook that changed shape, or an LLM that returned almost-valid JSON. Pydantic lets you describe the data you expect with ordinary Python type hints and validates it at runtime, with clear error messages. Version 2 rewrote the core in Rust (pydantic-core), making validation much faster.

Models from type hints

from datetime import datetime
from pydantic import BaseModel, Field, EmailStr   # EmailStr needs: pip install "pydantic[email]"

class OrderItem(BaseModel):
    sku: str = Field(min_length=3)
    qty: int = Field(gt=0, le=100)

class Order(BaseModel):
    customer_email: EmailStr
    items: list[OrderItem]
    created_at: datetime
    note: str | None = None

order = Order.model_validate({
    "customer_email": "[email protected]",
    "items": [{"sku": "BOOK-42", "qty": "2"}],   # "2" is converted to int 2
    "created_at": "2026-10-04T10:15:00Z",
})
print(order.items[0].qty + 1)    # 3

Invalid data raises a ValidationError that lists every problem with its location, such as items.0.qty: Input should be greater than 0, which you can return to API clients directly.

Custom rules

from pydantic import field_validator, model_validator

class Signup(BaseModel):
    username: str
    password: str
    password_again: str

    @field_validator("username")
    @classmethod
    def lowercase_slug(cls, v: str) -> str:
        if not v.replace("-", "").isalnum():
            raise ValueError("letters, numbers and dashes only")
        return v.lower()

    @model_validator(mode="after")
    def passwords_match(self):
        if self.password != self.password_again:
            raise ValueError("passwords do not match")
        return self

Serialising

order.model_dump()                     # dict
order.model_dump_json(exclude_none=True)
Order.model_validate_json(raw_bytes)   # parse and validate JSON in one fast step

Strict vs lax mode

By default Pydantic converts compatible types (the string "2" becomes 2). For payloads where silent conversion could hide a bug, use strict mode, per field or per model:

from pydantic import ConfigDict

class Payment(BaseModel):
    model_config = ConfigDict(strict=True, extra="forbid")
    amount_cents: int
    currency: str

extra="forbid" also rejects unknown fields, which catches client typos like ammount_cents.

Validating things that aren't models

TypeAdapter validates any type, which is handy for lists or unions at a boundary:

from pydantic import TypeAdapter

ids = TypeAdapter(list[int]).validate_python(["1", 2, "3"])   # [1, 2, 3]

Settings from the environment

The companion package pydantic-settings loads configuration from environment variables and .env files with the same validation:

from pydantic_settings import BaseSettings

class Settings(BaseSettings):
    database_url: str
    request_timeout: float = 10.0
    debug: bool = False

settings = Settings()   # reads DATABASE_URL, REQUEST_TIMEOUT, DEBUG

A misconfigured deployment now fails loudly at startup instead of halfway through a request.

Validating LLM output

When you ask a model for JSON, validate it like any other untrusted input:

class TicketTriage(BaseModel):
    category: Literal["billing", "bug", "how-to", "other"]
    priority: int = Field(ge=1, le=4)
    summary: str = Field(max_length=280)

try:
    triage = TicketTriage.model_validate_json(llm_text)
except ValidationError as e:
    triage = retry_with_errors(llm_text, e.errors())   # feed the errors back once, then fall back

TicketTriage.model_json_schema() produces a JSON Schema you can pass to APIs that support structured outputs.

v1 to v2: the renames that trip people up

Pydantic v1 Pydantic v2
.dict() / .json() .model_dump() / .model_dump_json()
parse_obj() / parse_raw() model_validate() / model_validate_json()
@validator / @root_validator @field_validator / @model_validator
class Config: model_config = ConfigDict(...)
BaseSettings in pydantic pydantic-settings package

Verdict

Put Pydantic at every boundary where data enters your code: HTTP requests, configuration, queues, third-party APIs and LLM responses. It's the cheapest way to turn "weird production bug" into "clear error at the door". If you use FastAPI, you're already halfway there, since it uses Pydantic models for request and response validation.

Written by

RecallRun Editors

Practical guides and independent tool overviews from the RecallRun team. Every post is written to be tested on your own machine.

Website

Written by RecallRun Editors for the RecallRun community. Community posts are checked for safety and reviewed by our editors before publishing, but the views and claims are the author's own. Links are the author's; open them with care. Report this post.

More from the community

Write for RecallRun

Share a tech article or a tool you built. Every post is checked and reviewed before it goes live.

Start writing