💻 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.

0.0
0Reviews
P
October 7, 2026

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.

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 - Result

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, checklist

Example 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.

Reviews (0)

Please login to leave a review.
Loading reviews...