💻 Coding
Pydantic v1 to v2 Migration Planner for FastAPI and Python Services: bump-pydantic First Pass, validator to field_validator, root_validator to model_validator, Config to ConfigDict, BaseSettings Move, and Behavior Changes
Migrate a codebase from Pydantic v1 to v2 without silent behavior changes: run bump-pydantic first, convert validators and root validators, move Config to model_config with renamed keys, replace dict, json, parse_obj, and from_orm calls, move BaseSettings to pydantic-settings, and catch Optional default and strict coercion changes with targeted tests.
0Reviews
Prompt
Act as a Python backend engineer who has migrated several FastAPI and worker services from Pydantic v1 to v2, uses bump-pydantic for the mechanical pass, and knows which changes it cannot make for you: validator signatures, Optional defaults, and coercion that v1 did quietly. Inputs: - Current versions: Python, pydantic, FastAPI or other frameworks that depend on pydantic, and the target versions: [Versions] - Pasted model code: the models, validators, Config classes, and settings classes to migrate: [ModelCode] - Call sites: where the code uses .dict(), .json(), parse_obj, parse_raw, from_orm, schema(), copy(), or __fields__: [CallSites] - Test coverage and CI notes: what tests exist around these models and which payloads are known in production: [TestNotes] - Rollout constraints: big bang or gradual, other services that import these models, a deadline: [Rollout] - Output format: [Format] Generate: 1. A plan header: dependency changes from Versions (pydantic 2, pydantic-settings if BaseSettings is used, a FastAPI release that supports v2), and whether to use the pydantic.v1 namespace for a gradual path given Rollout. 2. bump-pydantic first pass: the command to run on the package and a list of what it will and will not rewrite in ModelCode. 3. Validator conversions: each @validator to @field_validator with @classmethod, each pre=True to mode='before', each always=True to validate_default, each values argument to info.data via ValidationInfo, and each @root_validator to @model_validator with the right mode. 4. Config conversions: class Config to model_config = ConfigDict(...) with renamed keys (orm_mode to from_attributes, allow_population_by_field_name to populate_by_name, anystr_strip_whitespace to str_strip_whitespace, schema_extra to json_schema_extra), and json_encoders replaced by field_serializer where needed. 5. Field changes: regex to pattern, const removed, __root__ models to RootModel, and Optional fields with no default that become required. 6. Call site replacements from CallSites: dict to model_dump, json to model_dump_json, parse_obj to model_validate, parse_raw to model_validate_json, from_orm to model_validate with from_attributes, schema to model_json_schema, copy to model_copy, __fields__ to model_fields. 7. Behavior change tests from TestNotes: payloads that v1 coerced (an int into a str field), Optional without default, validator order, and serialized output shape, each as a pytest case. 8. A rollout checklist: order of services, the diff to review, and what to watch in logs after deploy. Constraints: - Show before and after code for every model in ModelCode. Do not invent fields or call sites; mark anything unclear as CHECK. - Mark API names that changed between minor v2 releases as CHECK against the installed version. No em dashes.
Instructions
Replace every [bracket] with your details before running. Works on ChatGPT, Claude, and Gemini.
Generated Output
This image was generated using the prompt above.

Examples
Example Input
Versions: Python 3.11, pydantic 1.10.13, FastAPI 0.95.2; target pydantic 2.x and a FastAPI release that supports it
ModelCode:
class Order(BaseModel):
id: int
sku: constr(regex=r'^[A-Z]{3}-\d{4}$')
note: Optional[str]
qty: int
@validator('qty')
def qty_positive(cls, v):
if v <= 0: raise ValueError('qty must be positive')
return v
@root_validator
def check_bulk(cls, values):
if values.get('qty', 0) > 100 and not values.get('note'): raise ValueError('bulk orders need a note')
return values
class Config:
orm_mode = True
class Settings(BaseSettings):
database_url: str
class Config:
env_prefix = 'APP_'
CallSites: routes use order.dict(exclude_none=True); worker uses Order.parse_raw(msg) and Order.from_orm(row)
TestNotes: 40 unit tests; some producers send id as a string like "123"
Rollout: single repo, one service, deploy next sprint
Format: plan, before and after code, tests, checklistExample Output
Plan
- pip install "pydantic>=2,<3" pydantic-settings and upgrade FastAPI to a release that supports v2. CHECK the FastAPI release notes for the exact minimum.
- One service, one repo: migrate in one pass; no pydantic.v1 shim needed.
bump-pydantic
- pip install bump-pydantic; bump-pydantic app/
- It handles Config to model_config, many imports, and simple validator renames. Review by hand: validator bodies that use values, the root validator, Optional defaults, and call sites.
After
from typing import Optional
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
from pydantic_settings import BaseSettings, SettingsConfigDict
class Order(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: int
sku: str = Field(pattern=r'^[A-Z]{3}-\d{4}$')
note: Optional[str] = None # v1 made this default None; v2 would make it required
qty: int
@field_validator('qty')
@classmethod
def qty_positive(cls, v: int) -> int:
if v <= 0:
raise ValueError('qty must be positive')
return v
@model_validator(mode='after')
def check_bulk(self) -> 'Order':
if self.qty > 100 and not self.note:
raise ValueError('bulk orders need a note')
return self
class Settings(BaseSettings):
model_config = SettingsConfigDict(env_prefix='APP_')
database_url: str
Why mode='after': the v1 root validator ran after field validation and read values; in v2 an after validator gets the built model, so use attributes.
Call sites
- order.dict(exclude_none=True) to order.model_dump(exclude_none=True)
- Order.parse_raw(msg) to Order.model_validate_json(msg)
- Order.from_orm(row) to Order.model_validate(row) (works because from_attributes=True)
Behavior tests (pytest)
def test_id_string_still_parses():
assert Order.model_validate({'id': '123', 'sku': 'ABC-1234', 'qty': 1}).id == 123
def test_note_optional():
assert Order(id=1, sku='ABC-1234', qty=1).note is None
def test_bulk_needs_note():
with pytest.raises(ValidationError):
Order(id=1, sku='ABC-1234', qty=101)
def test_dump_shape():
assert Order(id=1, sku='ABC-1234', qty=2).model_dump(exclude_none=True) == {'id': 1, 'sku': 'ABC-1234', 'qty': 2}
Note: string "123" into int still parses in v2 lax mode; an int into a str field no longer does. CHECK producers for that case.
Rollout checklist
1. Branch, run bump-pydantic, apply the hand edits above, run the 40 tests plus these 4.
2. Grep for .dict(, .json(, parse_obj, __fields__ left behind.
3. Deploy; watch logs for ValidationError spikes on the worker queue for one day.