Cheatsheet for migrating Pydantic v1 code to v2.

Method renames

v1v2
parse_obj(data)model_validate(data)
parse_raw(json_str)model_validate_json(json_str)
parse_file(path)model_validate_json(Path(...).read_text())
dict()model_dump()
json()model_dump_json()
copy()model_copy()
construct()model_construct()
update_forward_refs()model_rebuild()
Config (class)model_config (dict)

Config

v1:

class Config:
    orm_mode = True
    allow_population_by_field_name = True
    use_enum_values = True

v2:

model_config = {
    "from_attributes": True,                # renamed from orm_mode
    "populate_by_name": True,
    "use_enum_values": True,
}

Validators

v1:

from pydantic import validator, root_validator

@validator("name")
def name_must_be_long(cls, v):
    return v

@root_validator
def check(cls, values):
    return values

v2:

from pydantic import field_validator, model_validator

@field_validator("name")
@classmethod
def name_must_be_long(cls, v: str) -> str:
    return v

@model_validator(mode="after")
def check(self):
    return self

Always @classmethod decorator on field_validator. model_validator(mode="after") takes / returns self.

Field

v1:

field: int = Field(..., min_value=0)

v2:

field: int = Field(..., ge=0)

Renamed: min_valuege, max_valuele, gt, lt same.

Optional / nullable

Mostly the same, but Mapped[int] semantics tightened in v2.

ValidationError

v1: from pydantic import ValidationError (same import).

v2: error structure differs. loc is now a tuple; type is a string code; msg is the message.

Models with __init__

v1: subclassing with custom __init__ worked.

v2: prefer model_validator(mode="before") instead of overriding __init__.

from_orm

v1: User.from_orm(sa_user).

v2: User.model_validate(sa_user) with from_attributes=True.

fields

v1: Model.__fields__ is a dict.

v2: Model.model_fields is a dict of FieldInfo.

Settings

v1: from pydantic import BaseSettings.

v2: from pydantic_settings import BaseSettings (separate package).

uv add pydantic-settings

Config also moves:

# v2
from pydantic_settings import SettingsConfigDict

class Settings(BaseSettings):
    model_config = SettingsConfigDict(env_file=".env", env_prefix="MYAPP_")

Json type

v1: Json[T] and Json.

v2: Json[T] works; standalone Json removed.

Strict types

v1: StrictInt, StrictStr, etc. (still exist).

v2: prefer Annotated[int, Strict()] syntax.

Aliases

v1: Field(alias="...").

v2: same, plus validation_alias, serialization_alias, AliasChoices, AliasPath.

TypeAdapter

v1: parse_obj_as(List[User], data).

v2: TypeAdapter(list[User]).validate_python(data).

Generics

v1: BaseModel, GenericModel.

v2: BaseModel, Generic[T] (no separate GenericModel).

# v2
from typing import TypeVar, Generic
T = TypeVar("T")

class Page(BaseModel, Generic[T]):
    items: list[T]

RootModel

v1: __root__ field.

v2: RootModel[T] class.

class Names(RootModel[list[str]]):
    pass

Codemod: bump-pydantic

uv pip install bump-pydantic
bump-pydantic src/

Auto-applies many renames. Not perfect; review changes.

Compatibility shim

from pydantic.v1 import BaseModel as V1BaseModel

For gradual migration. v1 BaseModel coexists.

What might break

  • Field ordering for model_validator calls.
  • Strict mode default changed (some types stricter).
  • __init_subclass__ interactions.
  • Custom validators relying on v1 internals.

Testing the migration

pytest -x
mypy src
ruff check src

Plus integration tests against your real flows. Common breakage: ORM conversion (from_ormmodel_validate + from_attributes).

Common mistakes

  • Mixing v1 and v2 code without pydantic.v1 shim.
  • Skipping codemod review — silent wrong changes.
  • Forgetting populate_by_name=True after rename.
  • Not updating FastAPI version (FastAPI ≥ 0.100 for Pydantic v2).

You finished

20 Pydantic cheatsheets done. Pair with:

If you want my v1→v2 migration template + codemod script, it’s at rajpoot.dev .


Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .