You have a Pydantic v1 model that works perfectly in your FastAPI app. You upgrade to v2 — or try to — and suddenly your validators break, your serialization output looks different, and FastAPI throws a deprecation warning you cannot silence. You are not alone. Pydantic’s v1-to-v2 migration is one of the most disruptive library upgrades in the Python ecosystem, and now that FastAPI has officially dropped v1 support, the pressure to migrate is real.
In this tutorial, you will learn to:
- Understand what Pydantic v2’s Rust core changed about validation performance and behavior
- Identify the breaking changes that fail silently in v1 but error in v2
- Migrate FastAPI applications from Pydantic v1 to v2 with real code examples
- Apply new serialization controls and stricter type coercion rules introduced in v2
- Prepare for future Pydantic upgrades as FastAPI’s minimum version climbs
Page Contents
What Pydantic v2 Changed — And Why It Matters for Your APIs
Pydantic v2 is not an incremental update — it is a ground-up rewrite. The validation engine was moved to Rust (via pydantic-core), which brought 5× to 50× performance improvements over v1 depending on the workload. But performance is not the main story. The bigger story is correctness: v2 is stricter about type coercion, serialization, and validator behavior. Things that v1 silently accepted — string-to-int coercion, unknown fields in JSON, generic model inheritance — now raise explicit errors.
If you are building new APIs, use v2 from day one. If you are maintaining a v1 codebase, the migration is mandatory — FastAPI 0.126.0 (December 2025) dropped support for Pydantic v1 entirely, requiring pydantic >= 2.7.0 as the minimum.
Prerequisites
This article assumes you are comfortable with Python type hints and have used Pydantic v1 or FastAPI. If you are brand new to Pydantic, our Python type hints guide is a good starting point. For FastAPI fundamentals, see our FastAPI vs Flask vs Django comparison.The Rust Core — What Changed Under the Hood
Pydantic v2’s most significant architectural change is the separation of the Python wrapper from the validation engine. The core logic lives in pydantic-core, a Rust binary that handles all heavy validation. Your Python Pydantic models call into this Rust library via bindings.
This has three practical implications:
- Speed. Rust’s zero-cost abstractions and memory safety make validation 5× to 50× faster than v1. Benchmarks from the Pydantic team show complex nested models validating 20× faster. For high-throughput APIs handling thousands of requests per second, this is significant.
- Strictness. The Rust core does not silently coerce types the way v1’s Python code sometimes did. A string passed where an integer is expected will raise a
ValidationErrorin v2 rather than automatically converting it. - Serialization overhaul. Methods like
.dict()and.json()are replaced by.model_dump()and.model_dump_json(). The new methods handle complex scenarios like serializing ORM objects, datetime subclasses, and custom types more consistently.
# v1 — automatic string to int coercion (silently works)
from pydantic import BaseModel, validator
class UserV1(BaseModel):
age: int
user = UserV1(age="25") # v1 coerces "25" to 25
print(user.age) # 25
# v2 — strict mode, string to int raises ValidationError
from pydantic import BaseModel
class UserV2(BaseModel):
age: int
user = UserV2(age="25") # pydantic_core.str validator... error
# Traceback (most recent call last):
# 1 validation error for UserV2
# age
# Input should be a valid integer [type=int_type]
Config Class Is Gone — Use model_config Instead
In Pydantic v1, you configured models using an inner Config class:
# Pydantic v1 — inner Config class
from pydantic import BaseModel
class ItemV1(BaseModel):
name: str
price: float
class Config:
allow_mutation = False
extra = "forbid"
In Pydantic v2, the inner Config class is removed. You use model_config — a ConfigDict — at class level:
# Pydantic v2 — model_config with ConfigDict
from pydantic import BaseModel, ConfigDict
class ItemV2(BaseModel):
name: str
price: float
model_config = ConfigDict(
allow_mutation=False,
extra="forbid"
)
The mapping is straightforward — class Config becomes model_config = ConfigDict(...). But note that ConfigDict must be imported from pydantic, and the keys are strings, not class attributes. This catches a common migration mistake where developers forget the quotes around keys like "forbid".
Validators — @validator Is Now @field_validator
Pydantic v1 used the @validator decorator from pydantic. In v2, it is replaced by @field_validator from pydantic, and the import path changes:
# Pydantic v1 — @validator
from pydantic import BaseModel, validator
class UserV1(BaseModel):
name: str
age: int
@validator("age")
def validate_age(cls, v):
if v < 0:
raise ValueError("Age must be positive")
return v
# Pydantic v2 — @field_validator
from pydantic import BaseModel, field_validator
class UserV2(BaseModel):
name: str
age: int
@field_validator("age")
@classmethod
def validate_age(cls, v):
if v < 0:
raise ValueError("Age must be positive")
return v
Two important differences:
- The
@field_validatordecorator requires@classmethod— v1 validators did not need it because they were implicitly class methods. - The decorator syntax is
@field_validator("field_name")— not a list, not multiple calls. For multiple fields, use@field_validator("field1", "field2"). - The mode parameter lets you control when validation runs:
@field_validator("age", mode="before")runs before type coercion; default is"after".
Serialization — .dict() and .json() Are Gone
Pydantic v2 replaces all serialization methods:
| v1 Method | v2 Replacement | Notes |
|---|---|---|
.dict() | .model_dump() | Returns a Python dict |
.json() | .model_dump_json() | Returns a JSON string |
.copy() | use .model_copy() | Deep copy with updates |
BaseModel.__fields__ | BaseModel.model_fields | Field definitions |
# v2 serialization — model_dump and model_dump_json
from pydantic import BaseModel
from datetime import datetime
class EventV2(BaseModel):
title: str
timestamp: datetime
event = EventV2(title="PyCon", timestamp=datetime(2026, 6, 15))
print(event.model_dump()) # {'title': 'PyCon', 'timestamp': datetime(...)}
print(event.model_dump_json()) # '{"title": "PyCon", "timestamp": "2026-06-15T00:00:00"}'
print(event.model_dump(mode="json")) # dict with JSON-serializable values
The mode="json" argument on model_dump converts non-JSON-native types (like datetime) to JSON-compatible strings — a common need when returning responses from FastAPI.
Strict Type Coercion — The Silent Failures
This is the most disruptive change for API developers. Pydantic v1 was permissive — it would coerce a string "42" to an integer 42 silently. Pydantic v2 in strict mode (the default) refuses this coercion and raises a ValidationError.
# v2 — strict type coercion, default behavior
from pydantic import BaseModel
class OrderV2(BaseModel):
order_id: int
amount: float
# This works in v1, fails in v2:
order = OrderV2(order_id="1024", amount="99.99")
# ValidationError: order_id
# Input should be a valid integer [type=int_type]
# ValidationError: amount
# Input should be a valid number [type=float_type]
If you need v1-style coercion (for example, when parsing external API responses with stringified numbers), you can enable lax mode via ConfigDict at the class level:
# v2 — enable lax (coercing) mode via model_config
from pydantic import BaseModel, ConfigDict, Field
class OrderV2Lax(BaseModel):
order_id: int = Field(coerce_numbers_to_str=True)
amount: float = Field(coerce_numbers_to_str=True)
model_config = ConfigDict(
coerce_numbers_to_str=True, # affects all fields
extra="ignore"
)
# Now "1024" and "99.99" are accepted and coerced
order = OrderV2Lax(order_id="1024", amount="99.99")
print(order.order_id) # 1024
print(order.amount) # 99.99
The coerce_numbers_to_str option is available in Pydantic v2.9+. For older versions, or when you want per-field control, use a @field_validator with mode="before" to manually coerce inputs before validation.
Unknown Fields and extra — “allow” vs “forbid”
In Pydantic v1, extra fields were ignored by default — extra = "allow" was the default. In v2, the default is extra = "ignore" (equivalent to v1’s “allow” but silently discards unknown fields). If you want strict behavior:
# v2 — forbid unknown fields
from pydantic import BaseModel, ConfigDict
class StrictUser(BaseModel):
name: str
email: str
model_config = ConfigDict(extra="forbid")
user = StrictUser(name="Alice", email="[email protected]", age=30)
# ValidationError: Input validation error
# field required: age
Generic Models — BaseModel[Ts] Is Now Generic
Pydantic v1 had a confusing pattern for generic models:
# Pydantic v1 — generic model pattern (confusing)
from pydantic import BaseModel, GenericModel
class ResponseV1(GenericModel):
data: str
code: int
# Pydantic v2 — native generic support, no GenericModel needed
from pydantic import BaseModel
from typing import Generic, TypeVar
T = TypeVar("T")
class ResponseV2(BaseModel, Generic[T]):
data: T
code: int
# Concrete specialization
StringResponse = ResponseV2[str]
response = StringResponse(data="success", code=200)
print(response.data) # "success"
In v2, GenericModel is deprecated. Use standard Python typing.Generic with BaseModel directly. The type parameter is now resolved at specialization time, making the pattern more predictable.
FastAPI Compatibility — Where Things Stand Today
FastAPI has a close relationship with Pydantic — the framework uses Pydantic models to define request/response schemas, path parameters, and query parameters. FastAPI officially supports Pydantic v2 and tests against every Pydantic release.
As of FastAPI 0.126.0 (December 20, 2025), Pydantic v1 support is fully dropped. The minimum required version is now pydantic >= 2.7.0. If you are still on pydantic.v1 imports, FastAPI will raise a deprecation warning in 0.127.0 and drop support entirely in the next release.
# Old — Pydantic v1 import pattern (deprecated)
from pydantic import BaseModel
from pydantic.v1 import validator # OLD — will break
# New — Pydantic v2 pattern
from pydantic import BaseModel, field_validator # CORRECT
FastAPI’s own migration guide is at fastapi.tiangolo.com/how-to/migrate-from-pydantic-v1-to-pydantic-v2. The key steps are:
- Update
pydanticto>= 2.7.0 - Replace all
pydantic.v1imports with directpydanticimports - Update
@validatorto@field_validator - Replace
.dict()and.json()with.model_dump()and.model_dump_json() - Update any
Configinner classes tomodel_config = ConfigDict(...)
Migration Checklist — From v1 to v2
Here is a practical migration checklist with the most impactful changes:
| Change | v1 | v2 |
|---|---|---|
| Config | class Config: ... | model_config = ConfigDict(...) |
| Field definition | from pydantic import Field | No change — same import |
| Validator | @validator("field") | @field_validator("field", mode="after") |
| Serialize to dict | .dict() | .model_dump() |
| Serialize to JSON | .json() | .model_dump_json() |
| Copy model | .copy() | .model_copy() |
| Generic model | GenericModel | Use typing.Generic[T] directly |
| Field names | __fields__ | model_fields |
| Extra fields | extra = "allow" | extra = "ignore" (default) |
What About Pydantic v3?
There is no Pydantic v3 released yet. As of April 2026, the current stable version is Pydantic v2.13.3. The Pydantic team has discussed v3 in GitHub issue #10033 and clarified that v3 will not be a large API rewrite like v2 was. Instead, v3 is expected to fix edge cases the team considers broken in v2 without changing the public API.
The recent v2.13 release (April 2026) included:
- Polymorphic serialization for model subclasses — serialized models can now include type discriminator fields automatically
exclude_iffor computed fields — conditionally exclude computed fields from serialization based on their value- ASCII-only string validation — validate that strings contain only ASCII characters
- Performance improvements to validation and serialization for complex nested models
These features represent the direction Pydantic is heading — deeper serialization control and stricter validation primitives. If you stay current with v2.x releases, the eventual v3 migration should be far less disruptive than the v1-to-v2 transition.
Common Mistakes and Gotchas
- Importing from
pydantic.v1— this namespace is deprecated and will be removed. Migrate all imports topydanticdirectly. - Using
.dict()in v2 — this method still exists but is deprecated. Use.model_dump()instead. - Forgetting
@classmethodon@field_validator— v2 requires it; v1 did not. - Unhandled generic models — if you used
GenericModelin v1, remove it and use standardtyping.Generic. - String-to-int coercion in strict mode — if your API receives stringified IDs from external clients, enable
coerce_numbers_to_str=TrueinConfigDictor pre-process inputs. - Config keys as bare words — in
ConfigDict(extra="forbid"), the value must be a string"forbid", not a bare word.
Practical Example — Migrating a FastAPI Endpoint
# Pydantic v1 — FastAPI endpoint with old patterns
from fastapi import FastAPI
from pydantic import BaseModel, validator
app = FastAPI()
class ProductV1(BaseModel):
name: str
price: float
class Config:
extra = "forbid"
@validator("price")
def validate_price(cls, v):
if v < 0:
raise ValueError("Price cannot be negative")
return v
@app.post("/products/")
async def create_product(product: ProductV1):
return product.model_dump()
# Pydantic v2 — migrated FastAPI endpoint
from fastapi import FastAPI
from pydantic import BaseModel, field_validator, ConfigDict
app = FastAPI()
class ProductV2(BaseModel):
name: str
price: float
model_config = ConfigDict(extra="forbid")
@field_validator("price")
@classmethod
def validate_price(cls, v):
if v < 0:
raise ValueError("Price cannot be negative")
return v
@app.post("/products/")
async def create_product(product: ProductV2):
return product.model_dump(mode="json")
The structural changes are minimal — Config class becomes model_config, @validator becomes @field_validator, and .model_dump() replaces .dict(). The behavior changes are where most migration effort goes: checking type coercion, extra field handling, and serialization output.
Summary and Next Steps
Pydantic v2’s Rust core delivers serious performance gains, but the stricter validation semantics are the real story for API developers. If you are on v1 today, the migration is not optional — FastAPI has dropped v1 support and the ecosystem is moving to v2 patterns. The good news: the migration is mechanical, well-documented, and the v2 API is stable.
Key things to remember:
- Replace
@validatorwith@field_validatorand add@classmethod - Replace
.dict()/.json()with.model_dump()/.model_dump_json() - Replace inner
Configclass withmodel_config = ConfigDict(...) - Audit type coercion — string-to-int that silently worked in v1 now errors in v2
- Watch for the eventual v3 release — the team has signaled it will not be a breaking API rewrite
To learn more about related topics, see our Python type hints guide and FastAPI vs Flask vs Django comparison. The official Pydantic migration guide is at pydantic.dev/docs/validation/latest/get-started/migration.
Frequently Asked Questions
Does Pydantic v3 exist?
No. As of April 2026, Pydantic v2.13.3 is the current stable version. The Pydantic team has discussed v3 in their GitHub issue #10033 and stated that v3 will not be a large API rewrite like v2 was — it will fix edge cases considered broken in v2. The ongoing v2.x releases (like v2.13) are where new features are shipping.
How much faster is Pydantic v2 compared to v1?
The Pydantic team reports 5× to 50× improvements depending on the workload. Simple models with basic types see smaller gains; deeply nested models with complex validation see larger ones. The Rust-based pydantic-core handles the heavy validation, which is where most of the speedup comes from.
Can I use pydantic.v1 alongside pydantic v2 in the same project?
Yes, during a migration period. You can import from pydantic.v1 for old code and pydantic (v2) for new code in the same project. However, FastAPI 0.126.0+ raises deprecation warnings for pydantic.v1 usage, and support will be fully dropped. Plan your migration to eliminate pydantic.v1 imports as soon as possible.
Does FastAPI officially support Pydantic v2?
Yes. FastAPI’s release notes state that Pydantic includes the tests for FastAPI with its own test suite, so new Pydantic versions above 1.0 are always compatible with FastAPI. FastAPI 0.126.0 (December 2025) made pydantic >= 2.7.0 the minimum version, fully dropping v1 support.

