Quick Reference · data validation & serialization · v2 · expanded

pydantic · cheat sheet

One idea underneath everything: your type annotations compile to a core schema (in Rust) that does two jobs — validate untrusted input into a typed instance, and serialize that instance back to a dict or JSON. Validators hook the way in; serializers hook the way out. This is v2 — every method is model_*. Expanded edition: nested & generic models, discriminated unions, JSON Schema, custom types, and FastAPI patterns.

define fields & types validate serialize custom logic config & advanced gotcha / v1 most common

Verified 2026-08-26 against Pydantic 2.13.4 / pydantic-core 2.46 (installed & run) & cross-checked with: docs.pydantic.dev (official) · github.com/pydantic/pydantic · the v1→v2 Migration Guide · PyPI

One schema, two directions — validate in, serialize out
RAW INPUT {"id": "1", "name": …} dict '{"id": 1, …}' JSON orm_obj (attributes) validate class User(BaseModel) id: int · name: str age: int = 0 ↳ compiled core schema · Rust @field_validator User(id=1, …) typed & guaranteed serialize OUTPUT .model_dump() → dict .model_dump_json() → str @field_serializer MOVING FROM v1? — the method renames that trip everyone up .dict().model_dump() .json().model_dump_json() .parse_obj().model_validate() .schema().model_json_schema() class Configmodel_config=ConfigDict @validator@field_validator @root_validator@model_validator .copy().model_copy() annotate the types · validate in · serialize out — the whole library is that round trip, plus hooks on each side
quickstart · define · validate · serialize
from pydantic import BaseModel, Field, field_validator
from datetime import datetime

class User(BaseModel):
    id: int
    name: str = "anon"
    age: int = Field(gt=0, le=120)          # constraint
    joined: datetime | None = None

    @field_validator("name")
    @classmethod
    def titlecase(cls, v): return v.title()

u = User.model_validate({"id": "1", "name": "ada", "age": 36})
# "1" coerced → 1 ; name → "Ada"
u.model_dump()          # {'id': 1, 'name': 'Ada', 'age': 36, 'joined': None}
u.model_dump_json()     # '{"id":1,"name":"Ada",...}'
Part IModeldefine · nest · parametrize · manipulate
01Define a modelBaseModel
02Nested & recursivemodels within models
03Generic modelsTypeVar · Generic
04Model utilitiescopy · compare · inspect
Part IIFields & Typesconstrain · annotate · specialize · alias
05Field()defaults & constraints
06The Annotated patternreusable metadata
07Common field typesstdlib annotations
08Pydantic typesbatteries included
09Custom & arbitrary typesextend the type system
10Aliasesexternal ↔ field names
Part IIIValidateraw input → guaranteed instance
11Validate inputraw → typed instance
12field_validatorper-field logic
13model_validatorwhole-model / cross-field
14Annotated validatorsreusable, on the type
15Errors & strictnesscoercion control
Part IVSerializeinstance → dict · JSON · schema
16Serialize / dumpinstance → dict / JSON
17Custom serializersshape the output
18computed_fieldderived output
19JSON SchemaOpenAPI / docs
Part VUnions, Config & Integratedispatch · configure · adapt · ship
20Unions & discriminatedone field, many shapes
21ConfigDictmodel_config = …
22TypeAdaptervalidate without a model
23Settings managementpydantic-settings
24Dataclasses & functionsvalidate beyond models
25FastAPI & real-worldits #1 use case
26v1 → v2 migrationwhat changed

Six ideas that make the rest obvious

Everything above is one of these patterns in disguise. Learn the shapes, not the method list.

one schema, two jobs

Annotations compile to a core schema that both validates input and serializes output. Same schema, opposite directions.

annotations core schema · Rust raw dict / json dict / json out validate serialize

validator modes: before · after · wrap

Where your function sits relative to core coercion. before sees raw input; after sees the coerced value; wrap straddles it.

raw before core coerce after (default) — post-coercion value wrap calls core itself — full control

field_validator vs model_validator

Scope decides which to use: one field's value, or the whole model where you can compare fields.

@field_validator email ← this one age name classmethod · one value return v @model_validator email · age · name all fields at once if a > b: raise self · cross-field return self

model_dump flags → different output

One instance, many shapes. The flags decide type, which keys, and which names appear.

instance model_dump() joined: datetime(…) python objects mode='json' joined: "2026-…" json-safe by_alias=True userId: 1 alias keys exclude_unset=True only fields the caller actually set → PATCH

discriminated unions dispatch by tag

A literal tag field routes the input straight to one member — no trial-and-error, and errors point at the right model.

{"pet_type": "dog",  "barks": 3} discriminator pet_type Cat 'cat' → Dog ✓ 'dog' → picked

lax vs strict coercion

By default Pydantic coerces compatible types. Strict mode refuses — the same input either converts or raises.

n: int = "5" lax strict 5 coerced → int ValidationError int_type turn on with ConfigDict(strict=True), StrictInt, or strict=True per call JSON parsing stays lenient even in strict mode for some types

Worth memorizing

everything is model_*.dict/.json/.parse_obj are v1-deprecated → model_dump/_json/validate
class Config is goneuse model_config = ConfigDict(...)
field_validator = classmethodbut model_validator(mode='after') is an instance method — return self
lax by default"5"→5; use strict=True or StrictInt to forbid coercion
mutable defaultsField(default_factory=list), never = []
Optional isn't optionalx: int | None still requires the key — add = None
mode='json' for JSON dictplain model_dump() keeps datetime/UUID as objects
exclude_unsetdumps only what was set — the PATCH-request pattern
prefer AnnotatedAnnotated[int, Field(gt=0)] beats x: int = Field(...)
TypeAdaptervalidate list[int] & friends with no model; build once, reuse
settings moved outpip install pydantic-settings — separate package in v2
before vs afterbefore sees raw input; after sees the coerced value
smart union picks bestnot first-match — use a discriminator or left_to_right for predictable dispatch
discriminated = tag fieldevery member needs a Literal tag; far faster than a plain union
recursive modelsquote the forward ref 'Node'; call model_rebuild() if unresolved
generics need subscriptBox[int](...) validates; bare Box leaves T as Any
model_copy skips validationlike model_constructupdate= sets fields unchecked
schema has two modesmodel_json_schema(mode='serialization')'validation'