# Complete Pydantic v1 → v2 migration guide: bump-pydantic, staged migration, and crushing 'silently breaking' changes

> Faithful to the Pydantic official migration guide, with real code and a verification path it explains mechanical renames with bump-pydantic, staged migration in the pydantic.v1 namespace, the correspondence tables for methods/config/validators, and how to crush 'silently breaking' breaking changes such as the abolition of Optional's implicit default, changes in type coercion, and behavior changes in union/regex.

- Published: 2026-06-26
- Author: 友田 陽大
- Tags: Python, Pydantic, 型安全, バリデーション, アーキテクチャ設計
- URL: https://tomodahinata.com/en/blog/pydantic-v1-to-v2-migration-complete-guide
- Category: Pydantic & type-safe validation
- Pillar guide: https://tomodahinata.com/en/blog/pydantic-v2-production-validation-type-safety

## Key points

- Think of migration in 2 layers. Mechanical renames (.dict→.model_dump, etc.) can be mostly automated with bump-pydantic, but the real risk is in silent breaking changes that 'work but behave differently.'
- The pydantic.v1 namespace is the key to staged migration. Bump to pydantic>=1.10.17, then install v2, run unmigrated modules with pydantic.v1, and coexist v1/v2.
- The biggest pitfall is Optional[T] becoming required. v1's implicit None default was abolished, and without explicit =None you get a missing error.
- Type coercion also changed: float→int only with zero fractional part, number→str off by default, Union is smart mode, regex is Rust-made with no lookaround. All can be partially restored with ConfigDict.
- BaseSettings, __root__, GetterDict, Constrained*, json_encoders, and GenericModel were moved/removed. Always prepare 'tests as a verification path' first before migrating.

---

## **Introduction: the real hard part of migration is that it "works"**

Pydantic v2 was officially released in 2023, with the validation core `pydantic-core` rewritten in Rust. Even so, many codebases still run on v1 — legacy systems, libraries whose maintenance stopped, and **v1-style code that generative AI tends to output.**

When you hear "migration," you may imagine the **mechanical work** of replacing `@validator` with `@field_validator` and `.dict()` with `.model_dump()`. This is certainly tedious, but it's not the essential hard part. It can be mostly automated with `bump-pydantic` described later.

**What's truly dangerous is the breaking changes that "work but behave quietly differently."** The meaning of `Optional[int]` changes, the conversion rules from `float` to `int` change, the resolution order of `Union` changes. The code passes without an error, and if tests are thin, the problem manifests **only in production, and in a different place at that.** This is the point where people bleed in legacy migration.

This article, faithful to the [official migration guide](https://pydantic.dev/docs/validation/latest/get-started/migration/), splits migration into **2 layers** — "① mechanical renames (automatable)" and "② silently breaking changes (no choice but to crush by hand)" — and explains with emphasis on the latter. For the design of Pydantic v2 itself, see the [Pydantic v2 practical guide](/blog/pydantic-v2-production-validation-type-safety).

---

## **1. Migration strategy: "coexist" in the `pydantic.v1` namespace**

Migrating a huge codebase all at once is reckless. v2 bundles the entire v1 API under the **`pydantic.v1` namespace**, letting you migrate in stages while **coexisting v1 and v2 in the same process.**

The procedure the official recommends is roughly the following.

1. **Raise the dependency floor**: change `pydantic<2` to **`pydantic>=1.10.17`.** This makes "the `pydantic.v1` namespace within v1" usable.
2. **Apply `bump-pydantic`** (next chapter). Auto-convert mechanical renames.
3. **Stash imports of unmigrated code**: for modules not yet migrated, replace `from pydantic.X import Y` with `from pydantic.v1.X import Y`.
4. **Install v2.** Migrated modules use the top-level `pydantic`, unmigrated modules use `pydantic.v1`, and they **run side by side.**
5. **Crush behavior changes by hand** (chapters 3–6).
6. **Enable both mypy plugins** (during the migration period).

```python
# 未移行コードは v1 名前空間で動かし続ける
from pydantic.v1 import BaseModel as BaseModelV1

# 移行済みコードは通常の v2
from pydantic import BaseModel
```

```toml
# pyproject.toml — 移行期は両方のプラグインを有効化
[tool.mypy]
plugins = ["pydantic.mypy", "pydantic.v1.mypy"]
```

> ⚠️ **Don't mix v1 and v2 models**: mixing a model inheriting `pydantic.v1.BaseModel` and a v2 `BaseModel` **within one model graph is not supported** (using a v1 model as a v2 generic's type argument, etc., is impossible). Separate cleanly per module and repack at the boundary.

---

## **2. Mechanical renames: bulk-convert with `bump-pydantic`**

First clear the part that can be automated. Use the official migration-support tool (beta).

```bash
pip install bump-pydantic
bump-pydantic my_package   # パッケージ配下のソースを一括変換
```

`bump-pydantic` is an "aid," not complete (the official doesn't guarantee completeness either). The behavior changes from chapter 3 onward must be fixed by hand. That said, it mechanically handles the following **method/attribute renames.**

| Pydantic v1 | Pydantic v2 |
| --- | --- |
| `.dict()` | `.model_dump()` |
| `.json()` | `.model_dump_json()` |
| `.parse_obj()` | `.model_validate()` |
| `.parse_raw()` | `.model_validate_json()` (`parse_raw` itself is deprecated) |
| `.copy()` | `.model_copy()` |
| `.construct()` | `.model_construct()` |
| `.schema()` / `.schema_json()` | `.model_json_schema()` |
| `.from_orm(obj)` | `.model_validate(obj)` (requires `from_attributes=True`) |
| `.update_forward_refs()` | `.model_rebuild()` |
| `__fields__` | `model_fields` |

Config (`Config`) keys are also bulk-conversion targets.

| Pydantic v1 (`class Config`) | Pydantic v2 (`model_config = ConfigDict(...)`) |
| --- | --- |
| `orm_mode = True` | `from_attributes=True` |
| `allow_population_by_field_name` | `populate_by_name` |
| `anystr_strip_whitespace` | `str_strip_whitespace` |
| `min_anystr_length` / `max_anystr_length` | `str_min_length` / `str_max_length` |
| `validate_all` | `validate_default` |
| `schema_extra` | `json_schema_extra` |
| `keep_untouched` | `ignored_types` |
| `allow_mutation = False` | `frozen=True` |

> 💡 **`class Config` itself is deprecated**: in v2, use `model_config = ConfigDict(...)`. On multiple inheritance, `model_config` is **merged**, and the rightmost base class wins conflicts.

---

## **3. Silently breaking ①: `Optional[T]` is no longer "default `None`"**

**This is the biggest and most dangerous change.** In v1, `x: Optional[int]` meant "omittable, default `None`." **In v2, `Optional[int]` means "allows `None` but the value is required."** The implicit default was abolished.

```python
from typing import Optional
from pydantic import BaseModel


class User(BaseModel):
    name: str                  # 必須・None 不可
    nickname: Optional[str]    # ⚠️ v2では「必須・None許容」。省略すると missing エラー！
    bio: Optional[str] = None  # ✅ 省略可能・None 許容（v1 の Optional の意味はこれ）
```

The official table clarifies these four quadrants.

| State | Field definition |
| --- | --- |
| Required, `None` not allowed | `f: str` |
| Required, `None` allowed | `f: Optional[str]` |
| Omittable, `None` not allowed, default `'abc'` | `f: str = 'abc'` |
| Omittable, `None` allowed, default `None` | `f: Optional[str] = None` |

> ⚠️ **The migration crux**: code that wrote `x: Optional[int]` in v1 and relied on the implicit `None` **fails in v2 with a "field is missing (missing)" error.** `Any` is the same and doesn't have an implicit `None` default in v2. **At migration, enumerate all `Optional` fields and explicitly add `= None` to those you want omittable.** You can mechanically pick these up with search, but `bump-pydantic` can't judge the meaning, so it can't automate it. It's the top thing to crush by hand.

---

## **4. Silently breaking ②: the rules of type coercion changed**

v2 made type coercion more rigorous and predictable. Some of the convenient "smart conversions" are disabled by default.

### **H3: `float` → `int` only when the fractional part is zero**

```python
class Model(BaseModel):
    x: int


Model(x=10.0)   # ✅ OK（小数部が 0）
Model(x=10.2)   # ❌ v2では ValidationError（v1 は 10 に切り捨てていた）
```

### **H3: number → string coercion is off by default**

```python
class Model(BaseModel):
    code: str


Model(code=123)   # ❌ v2では既定で ValidationError
# 復元したいなら：model_config = ConfigDict(coerce_numbers_to_str=True)
```

### **H3: `Union` is resolved in "smart mode"**

v1 tried Union members from left to right. v2's default is **smart mode** — it chooses the "most naturally fitting" type.

```python
class Model(BaseModel):
    value: int | str


Model(value="1").value   # v2: "1"（str のまま）／ v1: 1（int に変換されていた）
# 左から順の挙動に戻すなら：value: int | str = Field(union_mode="left_to_right")
```

### **H3: the regex engine became Rust-made**

v2's `pattern` uses Rust's `regex` crate, so **lookaround and backreference can't be used.**

```python
# Python の re に依存した正規表現を使うなら、エンジンを戻す
class Model(BaseModel):
    model_config = ConfigDict(regex_engine="python-re")
    code: str = Field(pattern=r"(?=.*\d)\w+")  # 先読みは python-re が必要
```

Other changes include the **abolition of automatic conversion of "an iterable of pairs → dict"** and the **normalization of collection subclasses (`Counter`, etc.) to a plain `dict`**, and so on. For the big picture of type coercion, also see the strict / lax section of the [Pydantic v2 practical guide](/blog/pydantic-v2-production-validation-type-safety).

---

## **5. Silently breaking ③: moved/removed features**

Cases where the feature itself was moved or deleted. Some can be noticed with an import error (= still better), but you need to grasp them.

| v1 feature | Handling in v2 | Response |
| --- | --- | --- |
| `from pydantic import BaseSettings` | **moved to a separate package** | `pip install pydantic-settings` ([dedicated guide](/blog/pydantic-settings-configuration-management-secrets-guide)) |
| `__root__` field | **abolished** | use `RootModel[...]` |
| `class Config: json_encoders` | **deprecated** | `@field_serializer` / `@model_serializer` |
| `orm_mode` / `GetterDict` | **abolished** | `from_attributes=True` |
| `Constrained*` classes like `ConstrainedInt` | **deleted** | `Annotated[int, Field(ge=0)]` |
| `pydantic.generics.GenericModel` | **deleted** | `class M(BaseModel, Generic[T])` |
| `Extra` enum | **deleted** | string literal of `ConfigDict(extra="forbid")` |
| `Field(const=...)` | **deleted** | use a `Literal[...]` type |
| `Field(min_items/max_items)` | **deleted** | `min_length` / `max_length` |
| `Color` / `PaymentCardNumber` | **to a separate package** | `pydantic-extra-types` |

```python
# 例：__root__ → RootModel、Constrained* → Annotated
from typing import Annotated
from pydantic import BaseModel, Field, RootModel

Tags = RootModel[list[str]]                  # was: class Tags(BaseModel): __root__: list[str]
PositiveInt = Annotated[int, Field(ge=0)]    # was: class PositiveInt(ConstrainedInt): ge = 0
```

> ⚠️ **Constraints don't automatically descend to generic arguments**: in v2, writing `list[str] = Field(pattern=".*")` **doesn't apply to each element.** To make it bite the elements, write `list[Annotated[str, Field(pattern=".*")]]`. A constraint written with the v1 feel "silently stops working" — a typical case, so be careful.

---

## **6. Migrating validators: changes in signature and behavior**

Validators have changes beyond renames.

```python
from typing import Any
from typing import Self
from pydantic import BaseModel, ValidationInfo, field_validator, model_validator


class SignupForm(BaseModel):
    email: str
    password: str
    password_repeat: str
    tags: list[int]

    # v1: @validator("email")  →  v2: @field_validator + @classmethod + mode
    @field_validator("email", mode="after")
    @classmethod
    def normalize_email(cls, value: str, info: ValidationInfo) -> str:
        # v1 の field/config 引数は廃止。info.field_name / info.config で参照する
        return value.strip().lower()

    # v1: @root_validator  →  v2: @model_validator。mode="after" は self を受け取る
    @model_validator(mode="after")
    def check_passwords(self) -> Self:
        if self.password != self.password_repeat:
            raise ValueError("パスワードが一致しません")
        return self
```

Main changes:

- **Explicitly add `@classmethod`** and specify **`mode=`** (`before` / `after` / `wrap` / `plain`).
- **`each_item=True` is abolished.** Per-element validation is via type annotation — `tags: list[Annotated[int, Field(ge=0)]]`.
- **The `field` / `config` arguments are abolished.** Reference via `info: ValidationInfo` (`info.data` / `info.field_name` / `info.config`).
- **`always=True` is abolished.** Substitute with `Field(validate_default=True)`.
- **`@root_validator` → `@model_validator`.** `mode="after"` receives **the instance (`self`), not a dict.**
- **`allow_reuse` became unnecessary** so delete it.
- **`@validate_arguments` → `@validate_call`** rename.

> ⚠️ **`TypeError` is no longer converted to `ValidationError`**: v1 wrapped a `TypeError` inside a validator into a `ValidationError`, but v2 **doesn't wrap it.** A raw `TypeError` surfaces on a signature mismatch, etc., so after migration review the validators' calling conventions.

---

## **7. Build the verification path first: make migration "provable"**

As we've seen, the danger of migration is that it "silently breaks." That's exactly why **preparing a verification path before starting the migration** is the iron rule (CLAUDE.md's "Verification First" principle).

1. **Write characterization tests first**: against the pre-migration v1 code, enrich tests that fix "this output / this error for this input." This becomes the safety net of migration.
2. **Flow the same input through v1 and v2 and take the diff**: run the v1 model in the `pydantic.v1` namespace and the v2 model at the top level in parallel, and compare the result of `model_dump()`. Spots where a diff appears are "silently broken" candidates.
3. **Try passing once with `strict=True`**: spots that relied on the change in type coercion (chapter 4) are visualized as a `ValidationError`.
4. **Enable both mypy plugins** and pick up type-level regressions too.

```python
# 移行の差分検出：同じ入力を v1 / v2 に流して出力を比較する
from pydantic.v1 import BaseModel as V1Model   # 旧定義
from app.models import User as V2User          # 新定義

def test_dump_parity():
    payload = {"name": "alice", "nickname": "al"}
    # v1 の出力を基準に、v2 が同じ形を返すことを固定する（差異があればここで落ちる）
    assert V2User.model_validate(payload).model_dump() == LegacyUser(**payload).dict()
```

For details on the testing strategy, see [Pydantic testing strategy](/blog/pydantic-testing-polyfactory-hypothesis-strategy-guide).

---

## **Conclusion: crush migration by splitting into "mechanical work" and "the gatekeeper of behavior"**

The Pydantic v1 → v2 migration becomes foreseeable if you think in 2 layers. Restating the key points of this article.

1. **Staged migration coexists v1/v2 in the `pydantic.v1` namespace** and proceeds per module (mixing is impossible within a model graph).
2. **Mechanical renames are mostly automated with `bump-pydantic`** (the method/config correspondence tables).
3. **`Optional[T]` becoming required** is the biggest pitfall — explicitly add `= None` to those you want omittable.
4. **Changes in type coercion** (float→int, number→str, Union smart mode, Rust regex) can be partially restored with `ConfigDict`.
5. **`BaseSettings` / `__root__` / `GetterDict` / `Constrained*` / `json_encoders` / `GenericModel`** were moved/removed. Replace per the correspondence tables.
6. **Validators** respond to `@classmethod` + `mode=`, and the abolition of `each_item`/`field`/`config`/`always`/`allow_reuse`.
7. As the premise for everything, **prepare the verification path (tests, v1/v2 diff, strict, mypy) first.**

"It works so it's fine" is the most dangerous judgment in migration. Start after **visualizing the behavior changes with tests**, and migration becomes not a risk but an investment to gain performance, ecosystem, and type safety.

As an official primary source, I strongly recommend reading through the [Migration Guide](https://pydantic.dev/docs/validation/latest/get-started/migration/) from this article's viewpoint.

---

### **Consulting on Pydantic v1 → v2 migration and legacy type-safety**

The author has evolved production Python backends without stopping them, starting with a B2B SaaS that won the Minister of Economy, Trade and Industry Award. Pydantic migration is not a mere version bump but a project of "safely crushing silently-breaking changes while visualizing them with a verification path." I carry out **building characterization tests, designing staged migration, detecting behavior diffs, and surrounding type-safety including FastAPI/SQLAlchemy**, quickly and at high quality with generative AI. For the Pydantic migration of a production system that can't be stopped, feel free to reach out.
