Pydantic v2 is a ground-up rewrite with a Rust core, and it shows - validation is noticeably faster. But the migration isn’t trivial. I spent about a day migrating a medium-sized FastAPI project, and here’s what I learned.

Use bump-pydantic first Link to heading

Before doing anything manual, run the automated migration tool:

pip install bump-pydantic
bump-pydantic .

This handles about 80% of the changes automatically. It renames methods, updates imports, and converts the obvious stuff. Review the diff carefully though - it occasionally makes mistakes with complex validators.

What bump-pydantic misses Link to heading

The tool doesn’t catch everything. Here’s what I had to fix manually:

Validator signature changes Link to heading

@validator becomes @field_validator, but the signature also changes:

# v1
@validator('name')
def validate_name(cls, v, values):
    return v.strip()

# v2 - note the @classmethod and changed signature
@field_validator('name')
@classmethod
def validate_name(cls, v: str) -> str:
    return v.strip()

If you need access to other fields, use @model_validator instead:

@model_validator(mode='after')
def validate_model(self) -> Self:
    # self.field1, self.field2 etc are available
    return self

Config class Link to heading

class Config becomes model_config:

# v1
class Model(BaseModel):
    class Config:
        orm_mode = True

# v2
class Model(BaseModel):
    model_config = ConfigDict(from_attributes=True)

Note that orm_mode was renamed to from_attributes.

Method renames Link to heading

These are straightforward but easy to miss in string searches:

v1v2
.dict().model_dump()
.json().model_dump_json()
.parse_obj().model_validate()
.parse_raw().model_validate_json()
__fields__model_fields

Field constraints Link to heading

Field(regex=...) becomes Field(pattern=...):

# v1
name: str = Field(regex=r'^[a-z]+$')

# v2
name: str = Field(pattern=r'^[a-z]+$')

Type annotation changes Link to heading

v2 prefers the modern union syntax:

# v1 style (still works but deprecated)
from typing import Optional
name: Optional[str] = None

# v2 style
name: str | None = None

Performance Link to heading

The Rust core makes a real difference. In my benchmarks on a model with nested objects and several validators, v2 was about 5-10x faster for validation. Serialisation (.model_dump()) was roughly 2x faster.

Worth the migration effort? Absolutely, especially if you’re validating lots of data.

For testing Pydantic models, see pytest tips.

Further reading Link to heading