New in 2026: Master Python for AI, Data Science

Python

Pydantic v2 — What Changed and Why Your APIs Need an Upgrade

Pydantic v2 — What Changed and Why Your APIs Need an Upgrade

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

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 ValidationError in 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_validator decorator 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 Methodv2 ReplacementNotes
.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_fieldsField 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 pydantic to >= 2.7.0
  • Replace all pydantic.v1 imports with direct pydantic imports
  • Update @validator to @field_validator
  • Replace .dict() and .json() with .model_dump() and .model_dump_json()
  • Update any Config inner classes to model_config = ConfigDict(...)

Migration Checklist — From v1 to v2

Here is a practical migration checklist with the most impactful changes:

Changev1v2
Configclass Config: ...model_config = ConfigDict(...)
Field definitionfrom pydantic import FieldNo 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 modelGenericModelUse typing.Generic[T] directly
Field names__fields__model_fields
Extra fieldsextra = "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_if for 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 to pydantic directly.
  • Using .dict() in v2 — this method still exists but is deprecated. Use .model_dump() instead.
  • Forgetting @classmethod on @field_validator — v2 requires it; v1 did not.
  • Unhandled generic models — if you used GenericModel in v1, remove it and use standard typing.Generic.
  • String-to-int coercion in strict mode — if your API receives stringified IDs from external clients, enable coerce_numbers_to_str=True in ConfigDict or 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 @validator with @field_validator and add @classmethod
  • Replace .dict() / .json() with .model_dump() / .model_dump_json()
  • Replace inner Config class with model_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.

Related posts
Python

Pydantic Agent Basics: A Complete 2026 Tutorial

ProgrammingPython

Production-Ready MCP Servers — Security, Testing & Deployment

ProgrammingPython

Build Your First MCP Server with Python SDK — Fundamentals

ProgrammingPython

Connect FastAPI to MCP — Two Integration Patterns

Leave a Reply