Pydantic v2 Data Validation and Modeling
Parent: Programming Languages · researched 2026-06-01T02:04:27.586Z· 10 sources · 10 concepts · skill pydantic-v2
> Reference file — part of the programming-languages hub. Created via /dr research (Pydantic v2 data validation and modeling).
Overview
- <!-- hub-reference-banner --> [source]
- > Reference file - part of the programming-languages hub. Created via /dr research (Pydantic v2 data validation and modeling). [source]
- > Sibling topics in this family are reference files under the hubs (programming-languages, software-engineering-patterns) - not standalone [source]
- > skills. Ignore any "use the X skill" / related_skills / SKIP pointers below that name a bare sibling [source]
- > skill; load that topic's references/<name>.md from the owning hub (see the hub's "Cross-hub map"). [source]
- > For general Python idioms, type hints, packaging, and async, see references/python-patterns.md in this hub. [source]
- > For the TypeScript/JS analog (runtime schema validation), see the top-level zod-schema-validation skill. [source]
- Pydantic v2 expert - runtime data validation and modeling in Python powered by [source]
- the Rust pydantic-core. Covers BaseModel and field definitions (Field, Annotated [source]
- constraints), the three validator modes (field_validator / model_validator, [source]
- before/after/wrap/plain), strict vs lax coercion and ConfigDict, serialization [source]
- (model_dump / model_dump_json, aliases, include/exclude, computed_field, RootModel), [source]
- TypeAdapter for non-model types, discriminated (tagged) unions, pydantic-settings [source]
- (BaseSettings, SettingsConfigDict, env/.env/secrets), ValidationError handling, and [source]
- V1→V2 migration plus performance anti-patterns. [source]
- TRIGGER: defining or validating Pydantic models; field_validator / model_validator; [source]
- Annotated constraints; strict mode / type coercion questions; model_dump / serialization / [source]
- aliases; TypeAdapter; discriminated unions; BaseSettings / config from env; migrating [source]
- Pydantic V1 → V2; Pydantic validation performance tuning. [source]
- SKIP: TypeScript/JS runtime validation - use zod-schema-validation; general Python idioms, [source]
- type hints, packaging, async - use python-patterns.md; pytest/Hypothesis testing — [source]
- use python-testing.md; API/REST design - use software-engineering-patterns. [source]
Overview
- Pydantic is the most widely used data-validation library for Python. It validates [source]
- data at runtime against Python type hints and produces structured, user-friendly [source]
- errors when data is invalid. Pydantic v2 (released mid-2023, stable and current [source]
- through 2026) rewrote the validation/serialization engine in Rust as a separate [source]
- package, pydantic-core (built with PyO3). The result is ~5–50× faster than [source]
- v1 (≈17× on a typical mixed-field model), with the Python layer reduced to schema [source]
- definition while the hot path runs in compiled Rust. [source]
- Three packages make up the ecosystem: [source]
- pydantic - the Python API (BaseModel, Field, validators, TypeAdapter). [source]
- pydantic-core - the Rust validation/serialization engine (not used directly). [source]
- pydantic-settings - BaseSettings for config from env vars, .env, secrets. [source]
- Use it when you need to parse untrusted input (API bodies, config, JSON, ORM [source]
- rows) into typed Python objects with guarantees, and serialize them back out. [source]
1. BaseModel and field definitions
- Subclass BaseModel; annotate fields with type hints. Validation runs on [source]
- construction and on the explicit model_validate* entry points. [source]
- Validation entry points: User(**data), User.model_validate(dict_or_obj), [source]
- User.model_validate_json(json_str_or_bytes). JSON parsing happens inside Rust [source]
- in model_validate_json - faster than json.loads() then model_validate. [source]
- Field(...) carries metadata/constraints: default, default_factory, [source]
- alias / validation_alias / serialization_alias, ge/gt/le/lt, [source]
- min_length/max_length, pattern, description, frozen, exclude. [source]
- Prefer Annotated[type, Field(...)] over field: type = Field(...) for [source]
- constraints. Constraints inside Annotated are compiled into the core schema and [source]
- run in Rust (no Python call overhead). They also compose with list[...], [source]
- dict[...], etc. (e.g. list[Annotated[int, Field(gt=0)]]). [source]
- from_attributes=True (in model_config, replaces v1 orm_mode) lets [source]
- model_validate read attributes off arbitrary objects (e.g. ORM rows). [source]
2. Validators — field, model, and the before/after/wrap modes
- Pydantic distinguishes validators (input → validated value) from serializers [source]
- (value → output). Validators run in a defined order around the core (Rust) validation. [source]
- Modes (the most-confused part of Pydantic v2): [source]
- mode="before" - runs on raw input before core coercion. Receives whatever [source]
- was passed (often a dict or str); use to reshape/normalize input. [source]
- mode="after" - runs on the already-validated, typed value. Safest default for [source]
- business rules; you get a real int/str/submodel, not raw input. [source]
- mode="wrap" - most powerful: receives the value and a handler callable; you [source]
- decide whether/when to call the inner validator and can transform around it. [source]
- mode="plain" - terminates validation; your function fully replaces core validation [source]
- for that field (no core coercion runs). [source]
- model_validator(mode="before") receives the raw input dict for the whole model; [source]
- mode="after" receives self (return self). Raise ValueError or AssertionError [source]
- inside a validator and Pydantic wraps it into a ValidationError. Validators can [source]
- take an info: ValidationInfo param for info.data (already-validated siblings), [source]
- info.context, info.field_name. [source]
- Reusable validators: attach a validator to a type once with [source]
- Annotated[str, AfterValidator(func)] / BeforeValidator / WrapValidator / [source]
- PlainValidator - cleaner than repeating @field_validator across models. [source]
3. Strict vs lax mode and ConfigDict
- By default Pydantic is lax: it coerces compatible types ("123" → 123, [source]
- "true" → True). Strict mode disables coercion and requires exact types. [source]
- Strictness is layered (most → least specific): per-call model_validate(..., strict=True) [source]
- > field-level Field(strict=True) / Strict() annotation > model_config. [source]
- Common ConfigDict keys: [source]
- strict, frozen (immutable + hashable; replaces v1 allow_mutation), [source]
- extra = "ignore" (default) / "forbid" / "allow", [source]
- validate_assignment=True (re-validate on attribute set; off by default), [source]
- from_attributes=True (ORM reads), populate_by_name=True (accept field name [source]
- and alias on input; renamed validate_by_name in newer versions), [source]
- str_strip_whitespace, use_enum_values, arbitrary_types_allowed, [source]
- json_schema_extra, ser_json_timedelta, etc. [source]
- model_config is a dict (ConfigDict(...)), not the v1 nested class Config. [source]
4. Serialization — model_dump, JSON, aliases, computed fields
- Key options (apply to all three): include / exclude (sets or nested dicts), [source]
- by_alias=True (use serialization_alias), exclude_unset (only fields explicitly [source]
- set - great for PATCH semantics), exclude_defaults, exclude_none, [source]
- round_trip=True, warnings="error", context=.... [source]
- Custom serializers: @field_serializer("foo", mode="plain"|"wrap") for one [source]
- field; @model_serializer for the whole model; Annotated[T, PlainSerializer(...)] [source]
- for reusable type-level serialization. [source]
- @computed_field - expose a derived @property in the serialized output: [source]
- RootModel[T] - a model whose top level is not an object (e.g. [source]
- RootModel[list[int]], RootModel[dict[str, User]]); replaces v1 __root__. [source]
5. TypeAdapter — validation/serialization without a BaseModel
- TypeAdapter brings Pydantic's machinery to any type - list[User], dict[str,int], [source]
- TypedDict, dataclasses, unions - without wrapping it in a model. Build the adapter [source]
- once (it compiles a core schema) and reuse it. [source]
- Use it for bulk validation of homogeneous collections (build the adapter at module [source]
- scope, not per call) and for validating request/response bodies that aren't models. [source]
6. Discriminated (tagged) unions
- Add a discriminator so the core validator picks one union member by a tag field [source]
- instead of trying each - faster, and produces one clean error instead of N. [source]
- For tags that aren't a plain field, use a callable discriminator via [source]
- Discriminator(func) - and handle both dict and model inputs inside it, since the [source]
- callable also runs during serialization. Discriminated unions also emit cleaner [source]
- OpenAPI/JSON-Schema. Non-discriminated unions use smart mode (best-match) by [source]
- default; left-to-right is available but usually worse. [source]
7. pydantic-settings — typed configuration
- BaseSettings populates fields from (priority high→low): **init kwargs → env vars → [source]
- .env file → secrets dir → field defaults**. [source]
- Nested config: env_nested_delimiter="__" maps APP_DB__HOST to db.host [source]
- (double underscore avoids clashing with names that contain single underscores). [source]
- Secrets: secrets_dir reads each file as one field's value (Docker/K8s secrets). [source]
- Customize sources by overriding settings_customise_sources (e.g. add a YAML or [source]
- vault source, reorder priority). [source]
- Best practice: .env for local dev only, commit a .env.example without [source]
- secrets, use real environment variables / secret stores in production. [source]
Tools / Frameworks
- FastAPI - built on Pydantic; request/response models are Pydantic models. [source]
- FastAPI ≥0.100 requires Pydantic v2. [source]
- bump-pydantic - automated V1→V2 codemod (renames @validator→@field_validator, [source]
- Config→model_config, .dict()→.model_dump(), etc.). Run it, then review diffs. [source]
- datamodel-code-generator - generate Pydantic models from JSON Schema / OpenAPI. [source]
- json_schema() / model_json_schema() - emit JSON Schema (draft 2020-12) for any [source]
- model or TypeAdapter, including for discriminated unions. [source]
- mypy / pyright - Pydantic ships a mypy plugin; v2 models type-check well with [source]
Methodology — choosing the right tool
- Validating a whole object with named fields? → BaseModel. [source]
- Validating a bare collection / TypedDict / union, no model needed? → TypeAdapter. [source]
- Reshaping raw input before typing? → @field_validator(mode="before") or a [source]
- model mode="before" validator. [source]
- Cross-field business rule on typed data? → @model_validator(mode="after"). [source]
- Same validation reused across models/types? → Annotated[T, AfterValidator(...)]. [source]
- A union you can tag? → discriminated union (Field(discriminator=...)). [source]
- App configuration? → BaseSettings from pydantic-settings. [source]
- Need exact types, no coercion (e.g. money, ids)? → strict=True (per-field or model). [source]
Practical Patterns
- Parse, don't validate-then-pass-dicts: convert at the boundary [source]
- (Model.model_validate_json(body)) and pass typed models inward. [source]
- PATCH/partial update: model_dump(exclude_unset=True) to send only fields the [source]
- Aliases for external naming: `Field(validation_alias="userId", [source]
- serialization_alias="user_id"); set populate_by_name=True` to also accept the [source]
- Python field name on input. [source]
- Immutable value objects: model_config = ConfigDict(frozen=True) → hashable, [source]
- usable as dict keys / in sets. [source]
- Bulk validation: build one module-level TypeAdapter(list[Model]) and call [source]
- validate_python once on the whole batch rather than looping per item. [source]
- Context-aware validation: Model.model_validate(data, context={...}), read via [source]
- info.context in validators (e.g. inject locale, feature flags). [source]
Anti-Patterns (and the fix)
- Overusing @field_validator for simple bounds. A Python validator always runs in [source]
- Python (function-call overhead, duplicates checks). → Use Annotated[int, Field(ge=0)] [source]
- so the constraint runs in Rust. [source]
- mode="before" when you wanted typed data. Before-validators get raw input [source]
- (often a str/dict), causing AttributeError/type bugs. → Use mode="after" for [source]
- rules on validated values. [source]
- Building a TypeAdapter inside a hot loop / per request. It recompiles the core [source]
- schema each time. → Construct once at module scope and reuse. [source]
- json.loads() then model_validate(dict). → model_validate_json(raw) parses [source]
- and validates in one Rust pass. [source]
- v1 carry-overs: class Config (→ model_config = ConfigDict(...)), @validator [source]
- (→ @field_validator), @root_validator (→ @model_validator), .dict() [source]
- (→ .model_dump()), .json() (→ .model_dump_json()), parse_obj (→ model_validate), [source]
- parse_raw (→ model_validate_json), from_orm/orm_mode [source]
- (→ model_validate(obj) + from_attributes=True), allow_mutation=False [source]
- (→ frozen=True), each_item=True (→ annotate the inner type). [source]
- Mutable default shared across instances (tags: list = []). → Field(default_factory=list). [source]
- Mixing Pydantic v1 and v2 models in one validation graph - they don't nest cleanly; [source]
- migrate the whole graph (bump-pydantic), or use the pydantic.v1 shim deliberately. [source]
- Expecting validate_assignment by default. It's off; set [source]
- ConfigDict(validate_assignment=True) if you mutate after construction. [source]
Troubleshooting
- ValidationError - iterate exc.errors() for structured dicts (loc, msg, [source]
- type, input); exc.json() / exc.error_count() for reporting. The type [source]
- string (e.g. int_parsing, missing, string_too_short) is the stable machine key. [source]
- "Input should be a valid integer" under strict mode - you passed a string to a [source]
- strict int; coerce upstream or drop strictness for that field. [source]
- Serialization warning "Expected X but got Y" - a field's runtime value doesn't [source]
- match its declared type (common with Any/subclasses); set [source]
- model_dump(serialize_as_any=True) for duck-typed/polymorphic output, or fix the type. [source]
- PydanticUndefinedAnnotation / forward refs - call Model.model_rebuild() after [source]
- the referenced type is defined (self-referential or late-bound models). [source]
- Settings not picked up - check env_prefix, the env_nested_delimiter, and that [source]
- .env is found (relative to CWD unless an absolute env_file path is given). [source]
- extra fields silently dropped - default is "ignore"; use "forbid" to catch [source]
- typos in input, "allow" to keep them. [source]
References
- Pydantic official docs - Validation (concepts, API): https://docs.pydantic.dev/latest/ [source]
- Pydantic - Migration Guide (V1 → V2): https://docs.pydantic.dev/latest/migration/ [source]
- Pydantic - Unions / discriminated unions: https://docs.pydantic.dev/latest/concepts/unions/ [source]
- Pydantic - Serialization: https://docs.pydantic.dev/latest/concepts/serialization/ [source]
- Pydantic - Strict mode: https://docs.pydantic.dev/latest/concepts/strict_mode/ [source]
- Pydantic - Performance: https://docs.pydantic.dev/latest/concepts/performance/ [source]
- pydantic-settings - Settings Management: https://docs.pydantic.dev/latest/concepts/pydantic_settings/ [source]
- Pydantic v2 announcement (architecture / Rust core): https://pydantic.dev/articles/pydantic-v2 [source]
- pydantic-core (Rust engine): https://github.com/pydantic/pydantic-core [source]
- bump-pydantic (V1→V2 codemod): https://github.com/pydantic/bump-pydantic [source]
Children
- BaseModel and field definitions (Field, Annotated constraints) (frontier)
- Validators (field_validator/model_validator, before/after/wrap/plain modes) (frontier)
- Strict vs lax coercion and ConfigDict (frontier)
- Serialization (model_dump/model_dump_json, aliases, include/exclude, computed_field, RootModel) (frontier)
- TypeAdapter for non-model types (frontier)
- Discriminated (tagged) unions (frontier)
- pydantic-settings (BaseSettings, env/.env/secrets) (frontier)
- ValidationError handling (frontier)
- pydantic-core (Rust) architecture and performance (frontier)
- V1 to V2 migration and anti-patterns (frontier)
Frontier under this node: BaseModel and field definitions (Field, Annotated constraints), Discriminated (tagged) unions, Serialization (model_dump/model_dump_json, aliases, include/exclude, computed_field, RootModel), Strict vs lax coercion and ConfigDict, TypeAdapter for non-model types, V1 to V2 migration and anti-patterns, ValidationError handling, Validators (field_validator/model_validator, before/after/wrap/plain modes), pydantic-core (Rust) architecture and performance, pydantic-settings (BaseSettings, env/.env/secrets)