# marshmallow 3 → 4 移行完全ガイド：破壊的変更を安全に乗り越える

> marshmallow 4の破壊的変更を公式アップグレードガイドに忠実に整理。missing/default→load_default/dump_default、pass_many→pass_collection、抽象基底クラスのインスタンス化禁止、validatorはValidationErrorをraise、Schema.context→contextvars、暗黙フィールド廃止までをbefore/afterで示し、段階的に移行する手順を解説します。

- 公開日: 2026-06-26
- 著者: 友田 陽大
- タグ: Python, marshmallow, マイグレーション, 型安全, テスト, アーキテクチャ設計
- URL: https://tomodahinata.com/blog/marshmallow-3-to-4-migration-guide

## 要点

- marshmallow 4はバリデーション中核に多数の破壊的変更を含む。本記事の対応表でbefore/afterを機械的に置換できる
- 最頻出はmissing/default→load_default/dump_defaultとpass_many→pass_collection。grepで全箇所を洗い出す
- validatorはFalseを返す方式が廃止され、必ずValidationErrorをraiseする方式に統一された
- Schema.contextは廃止。contextvars.ContextVar / experimental.Contextへ移行する
- バージョン固定とテストで退行を止めつつ、段階的に移行するのが安全。CIで担保する

---

## **導入：移行は「機械的置換 + テストで担保」という作業に分解できる**

「メジャーバージョンアップ」と聞くと身構えるかもしれません。しかし marshmallow 3 → 4 の移行は、**正体の分かっている有限個の破壊的変更**の集合です。新しい概念を学ぶというより、**既存コードの特定パターンを別のパターンへ機械的に置き換え、テストで退行（リグレッション）が起きていないことを確かめる**——その作業に分解できます。本記事はその「対応表」と「手順書」です。

なぜ今あえて 4.x へ上げるのか。理由は機能追加ではなく、**保守とセキュリティ更新の継続性**にあります。3.x 系は保守モードに入っており、今後の不具合修正・依存ライブラリの追従・脆弱性対応は 4.x 系が主戦場です。古いバージョンに留まり続けることは、いずれ「サポート切れの基盤の上で本番を回す」という、それ自体が技術的負債でありリスクである状態に近づいていきます。**バージョンアップは、上げたい時ではなく、上げられる時に小さく済ませる**のが鉄則です。

> 💡 **前提知識**：marshmallow そのものの設計思想（`Schema` / `fields` / `load()` / `dump()` / バリデーションの三層 / セキュリティ境界）は、[marshmallow 実践ガイド](/blog/marshmallow-python-serialization-validation-production-guide) で体系的に解説しています。本記事は、その続編として **3 → 4 移行に特化したディープダイブ**です。各 API の意味が曖昧なときは、まずピラー記事に立ち返ってください。

> 💡 **この記事で扱うバージョン**：現行の安定版は marshmallow **4.3.0**（2026年4月時点）です。3.x 系は新機能追加が止まった保守モードにあります。本記事の before/after は、すべて公式の「[Upgrading to newer releases](https://marshmallow.readthedocs.io/en/stable/upgrading.html)」ガイドに基づく、4.x の破壊的変更です。

---

## **1. 移行戦略の全体像：4 ステップで「壊さずに上げる」**

行き当たりばったりに `pip install -U marshmallow` を実行し、大量のエラーに埋もれる——これが最悪の進め方です。安全な移行は、次の 4 ステップを**順番に**踏みます。各ステップは独立した小さな PR にし、レビューしやすく、問題が起きても切り戻しやすくします。

```text
① バージョン固定     現状の 3.x をロックし、移行作業を「いつでも中断・再開できる」状態にする
② 警告を潰す         3.x の最終版で DeprecationWarning をエラー化し、4.x で消える機能を先に直す
③ 4.x へ上げる        固定を外して 4.x に上げ、残った破壊的変更を対応表に従って置換する
④ テストで担保       load/dump の往復をゴールデンテストで固定し、CI を緑にして完了とする
```

### **① まず 3.x にバージョンを固定する**

移行を始める前に、現在動いているバージョンを明示的に固定します。これにより「いつの間にか勝手に 4.x が入って壊れた」という事故を防ぎ、移行を**自分の意思でコントロールできる作業**に変えます。

```bash
# 現状を 3.x 系にロック（4.0 へ勝手に上がらないようにする）
pip install "marshmallow<4"

# requirements.txt / pyproject.toml にも明示的に書く
# marshmallow>=3,<4
```

### **② 3.x の最終版で「非推奨警告」を先に潰す**

4.x で削除される機能の多くは、3.x の後期バージョンで既に **`DeprecationWarning`** を出しています。**4.x へ上げる前に、3.x のまま警告をすべて消しておく**——これが移行を劇的に楽にする最大のコツです。警告が出ているコードは、4.x では「警告」ではなく「エラー」になるからです。

警告を確実に検出するには、Python の `-W error::DeprecationWarning` フラグで**警告を例外に昇格**させ、テストを走らせます。

```bash
# DeprecationWarning をエラー化してテストを実行 → 非推奨 API の使用箇所が例外として顕在化する
python -W error::DeprecationWarning -m pytest
```

pytest を使っているなら、設定ファイルでも同じことができます。

```ini
# pytest.ini / setup.cfg / pyproject.toml の [tool.pytest.ini_options]
[pytest]
filterwarnings =
    error::DeprecationWarning
```

> 💡 **なぜこのステップを分けるのか**：3.x のまま警告を潰せば、各修正が「4.x へ上げた後の挙動変化」と混ざらず、**原因の切り分けが圧倒的に容易**になります。`@post_dump(pass_many=True)` のような変更は 3.x の段階でも新名称を受け付けるものがあり、先に直しておけば 4.x 移行時の差分が小さくなります。「警告ゼロの 3.x」という中間地点を必ず経由してください。

### **③ 4.x へ上げて、残りの破壊的変更を置換する**

警告を潰し終えたら、固定を外して 4.x に上げます。ここで本記事の**第 2 章以降の対応表**を使い、残った破壊的変更を機械的に置換していきます。

```bash
pip install "marshmallow>=4,<5"
```

### **④ テストで退行を止める**

最後に、移行前後で `load()` / `dump()` の入出力が変わっていないことを**テストで固定**します（第 12 章で詳述）。型シグネチャは通っても、**出力 JSON の中身が静かに変わる**のが移行で最も怖い退行です。これを CI で機械的に検出できる状態を作って、初めて「移行完了」と呼べます。

---

ここからは、対応表の各行を **before / after** で 1 つずつ展開していきます。まず全体像を俯瞰したい場合は、本章末の早見表を先に眺めてから読み進めてください。

## **2. `missing` / `default` → `load_default` / `dump_default`（最頻出）**

**最も出現頻度が高く、最も重要な変更**です。3.x の `missing=`（load 時のデフォルト）と `default=`（dump 時のデフォルト）は、4.x では `load_default=` / `dump_default=` に改名されました。

```python
# ❌ 3.x（旧）
from marshmallow import Schema, fields


class UserSchema(Schema):
    name = fields.Str(missing="Monty")    # load 時に欠落していたらこの値
    answer = fields.Int(default=42)       # dump 時に欠落していたらこの値
```

```python
# ✅ 4.x（新）
from marshmallow import Schema, fields


class UserSchema(Schema):
    name = fields.Str(load_default="Monty")    # load 時のデフォルト
    answer = fields.Int(dump_default=42)        # dump 時のデフォルト
```

**なぜこれが優れているのか？**
3.x の `missing` / `default` という名前は、**「いつ」適用されるデフォルトなのかが名前から読み取れません**でした。`missing` は入力時、`default` は出力時——この対応を覚えていなければコードを誤読します。`load_default` / `dump_default` は、marshmallow の二大操作である `load()` / `dump()` と名前が一対一で対応します。「`load_default` は `load()` のデフォルト、`dump_default` は `dump()` のデフォルト」と、**名前を読むだけで意味が確定する**。これは「意図を明らかにする命名」という可読性原則の、教科書的な改善です。

> ⚠️ **意味の取り違えに注意**：「入力時のデフォルト」と「出力時のデフォルト」は別物です。`load_default` を設定すべき箇所に `dump_default` を書くと、欠落入力が補われずバリデーションの挙動が変わります。機械置換ではなく、**1 つずつ `load` 用か `dump` 用かを判断**してください（grep の使い方は第 10 章）。

---

## **3. 抽象基底クラスのインスタンス化禁止：`Number()` / `Mapping()` / `Field()` は使えない**

`fields.Number()`、`fields.Field()`、`fields.Mapping()` は**抽象基底クラス**であり、4.x では**直接インスタンス化できなくなりました**。具体的な型を表す具象フィールドを使います。

```python
# ❌ 4.x ではエラーになる（抽象基底クラスの直接利用）
from marshmallow import Schema, fields


class ProductSchema(Schema):
    price = fields.Number()       # 整数か小数か曖昧
    meta = fields.Mapping()       # キー・値の型が不明
    raw = fields.Field()          # 何でも通す
```

```python
# ✅ 4.x：意図する具象型を明示する
from marshmallow import Schema, fields


class ProductSchema(Schema):
    # Number → 整数 / 小数 / 10進数のいずれかを明示
    price = fields.Decimal(as_string=True)   # 金額なら Decimal が安全（誤差なし）
    quantity = fields.Integer()
    ratio = fields.Float()

    # Mapping → Dict にキー・値の型を与える
    meta = fields.Dict(keys=fields.Str(), values=fields.Str())

    # Field → 通したい型の具象フィールドを選ぶ（型を限定できないなら Raw を検討）
    raw = fields.Raw()
```

**なぜこれが優れているのか？**
`fields.Number()` のような抽象型は「整数なのか小数なのか」をスキーマ定義から読み取れず、検証も緩くなります。具象型への移行を強制されることは一見手間ですが、実際には**「このフィールドが本当は何型なのか」をコードに明記する好機**です。`price` が `Decimal` なのか `Float` なのかは、金額計算の正確性（誤差の有無）に直結する設計判断です。曖昧な基底クラスを禁じることで、marshmallow は**型の意図をスキーマに刻むこと**を促しています。これは型安全の原則そのものです。

---

## **4. バリデータは必ず `ValidationError` を `raise` する（`False` を返す方式は廃止）**

3.x では、バリデータ関数が **`False` を返す**ことで検証失敗を表現できました。4.x では**この方式は廃止**され、検証に失敗したら必ず `ValidationError` を **`raise`** しなければなりません。

```python
# ❌ 3.x（旧）：False を返して失敗を表現
from marshmallow import Schema, fields


class LoginSchema(Schema):
    # 一致しなければ False が返り、それが検証失敗として扱われていた
    secret = fields.Str(validate=lambda x: x == "password")
```

```python
# ✅ 4.x（新）：失敗時は ValidationError を raise
from marshmallow import Schema, fields, ValidationError


def must_be_password(value: str) -> None:
    if value != "password":
        # メッセージを持った例外を送出する（戻り値ではなく例外で失敗を伝える）
        raise ValidationError("合言葉が一致しません。")


class LoginSchema(Schema):
    secret = fields.Str(validate=must_be_password)
```

**なぜこれが優れているのか？**
`False` を返す方式には致命的な弱点が 2 つありました。第一に、**`None` を返したり、戻り値を書き忘れた関数が「成功」と誤判定される**——`return` を忘れた関数は暗黙に `None`（偽値ではない真偽不明値）を返し、検証が素通りします。これはバリデーション特有の、最も発見しにくいバグの一つです。第二に、**`False` には「なぜ失敗したか」の情報が乗りません**。`ValidationError` を `raise` する方式なら、失敗には必ずメッセージが伴い、戻り値の書き忘れは「例外を投げない＝成功」として明確に振る舞います。「失敗は戻り値ではなく例外で伝える」という規律は、エラーを握り潰しにくくする、信頼性のための正しい設計です。

---

## **5. デコレータ引数 `pass_many` → `pass_collection`**

`@pre_load` / `@post_load` / `@pre_dump` / `@post_dump` の各デコレータで、コレクション全体（`many=True` の入出力）を一括処理する際に使う引数が、`pass_many` から **`pass_collection`** に改名されました。

```python
# ❌ 3.x（旧）
from marshmallow import Schema, post_dump


class EnvelopeSchema(Schema):
    @post_dump(pass_many=True)
    def wrap(self, data, many, **kwargs):
        key = "results" if many else "result"
        return {key: data}
```

```python
# ✅ 4.x（新）：引数名だけが変わる（メソッドのシグネチャは同じ）
from marshmallow import Schema, post_dump


class EnvelopeSchema(Schema):
    @post_dump(pass_collection=True)
    def wrap(self, data, many, **kwargs):
        key = "results" if many else "result"
        return {key: data}
```

引数名の変更だけで、メソッドが `many` を受け取るシグネチャは変わりません。これは第 2 章の `load_default` と並ぶ**機械置換の代表例**で、`pass_many=` を `pass_collection=` に一括置換すれば済みます。「コレクション全体を渡す」という意図が `pass_collection` という名前で明確になった、自然な改名です。

---

## **6. 暗黙のフィールド生成は廃止：`class Meta: fields` / `additional` は使えない**

3.x では `class Meta` の `fields` タプルや `additional` を使い、**クラス属性として宣言していないフィールドを暗黙的に生成**できました。4.x ではこの仕組みが廃止され、**使うフィールドはすべて明示的に宣言**する必要があります。

```python
# ❌ 3.x（旧）：Meta.fields / additional による暗黙生成
from marshmallow import Schema, fields


class UserSchema(Schema):
    email = fields.Email()

    class Meta:
        # name / created_at は属性宣言なしに「いい感じに」生成されていた
        fields = ("name", "email", "created_at")
        additional = ("nickname",)
```

```python
# ✅ 4.x（新）：すべてのフィールドを明示宣言する
from marshmallow import Schema, fields


class UserSchema(Schema):
    name = fields.Str()
    email = fields.Email()
    created_at = fields.DateTime()
    nickname = fields.Str()
```

**なぜこれが優れているのか？**
`Meta.fields = ("name", ...)` で生成されるフィールドは**型が不明瞭**で、`name` が文字列なのか整数なのかをスキーマ定義から読み取れませんでした。明示宣言を強制することで、各フィールドの型・検証・`dump_only` などの属性がすべてスキーマ本体に現れます。これは可読性と型安全の両方を底上げします。なお、**SQLAlchemy モデルから自動生成したい**正当なユースケースは、`marshmallow-sqlalchemy` の `SQLAlchemyAutoSchema` が引き続き担います——「暗黙のフィールド生成」の廃止は、その正規ルートへの集約でもあります。

---

## **7. `Schema.context` 廃止 → `contextvars` / `experimental.Context`**

3.x では `schema.context = {...}` を通じて、シリアライズ／デシリアライズ処理に外部の文脈（リクエストユーザー、ロケールなど）を渡せました。4.x では **`Schema.context` は廃止**され、Python 標準の `contextvars.ContextVar` を使うか、marshmallow が提供する**実験的（experimental）な `Context` マネージャ**を使います。

```python
# ❌ 3.x（旧）：schema.context に状態を詰める
schema = UserSchema()
schema.context["current_user"] = current_user
result = schema.dump(user)
# メソッド内では self.context["current_user"] で参照していた
```

```python
# ✅ 4.x（新）：experimental.Context の with 構文で文脈を束縛する
from typing import TypedDict
from marshmallow import Schema, fields
from marshmallow.experimental.context import Context


class CtxDict(TypedDict):
    current_user: object


class UserSchema(Schema):
    name = fields.Str()
    # フィールドのシリアライズ中に Context.get() で文脈を取り出す
    is_self = fields.Function(
        lambda obj: obj is Context[CtxDict].get()["current_user"]
    )


# with ブロックの中だけ文脈が有効になる（ブロックを抜ければ自動で破棄）
with Context[CtxDict]({"current_user": current_user}):
    result = UserSchema().dump(user)
```

`contextvars` ベースの低レベルな書き方も可能です。

```python
# ✅ 4.x（新）：標準ライブラリの ContextVar を直接使う
import contextvars

_current_user: contextvars.ContextVar = contextvars.ContextVar("current_user")

# 値をセットしてから dump/load を呼ぶ
token = _current_user.set(current_user)
try:
    result = UserSchema().dump(user)
finally:
    _current_user.reset(token)   # 後始末を忘れない
```

> ⚠️ **`experimental.Context` は実験的 API**：名前のとおり安定保証の対象外で、将来のマイナーバージョンで変わる可能性があります。本番採用するなら、**呼び出しを薄いラッパー関数に閉じ込め**、仕様変更時の影響を一箇所に局所化しておくのが安全です。

**なぜこれが優れているのか？**
`schema.context` は**スキーマインスタンスに可変状態を持たせる**設計でした。同じスキーマインスタンスを使い回すと前の文脈が残り、並行処理では状態が混線します（典型的な共有可変状態のバグ）。`contextvars` は**実行コンテキストごとに独立した値**を保持するため、`async` / スレッドをまたいでも状態が漏れません。`with Context(...)` の構文は、**文脈の有効範囲がブロックとして可視化され、ブロックを抜ければ自動で破棄される**——スコープと生存期間が一致する、より安全で読みやすいモデルです。

---

## **8. `@validates` が複数フィールド対応＋メソッドが `data_key` を受け取る**

3.x の `@validates` は 1 つのフィールド名しか取れませんでした。4.x では**複数のフィールド名**を渡せるようになり、検証メソッドは**どのフィールドを処理中かを示す `data_key`** を引数で受け取ります。

```python
# ❌ 3.x（旧）：フィールドごとに同じ検証を重複定義しがち
from marshmallow import Schema, fields, validates, ValidationError


class ItemSchema(Schema):
    quantity = fields.Integer()
    reserved = fields.Integer()

    @validates("quantity")
    def validate_quantity(self, value):
        if value < 0:
            raise ValidationError("0 以上にしてください。")

    @validates("reserved")
    def validate_reserved(self, value):
        if value < 0:
            raise ValidationError("0 以上にしてください。")
```

```python
# ✅ 4.x（新）：複数フィールドを 1 メソッドで検証し、data_key でメッセージを動的化
from marshmallow import Schema, fields, validates, ValidationError


class ItemSchema(Schema):
    quantity = fields.Integer()
    reserved = fields.Integer()

    @validates("quantity", "reserved")
    def validate_non_negative(self, value, data_key):
        if value < 0:
            # data_key で「どのフィールドのエラーか」をメッセージに反映できる
            raise ValidationError(f"{data_key} は 0 以上である必要があります。")
```

**なぜこれが優れているのか？**
3.x では「同じルールを複数フィールドに適用する」たびにメソッドを複製する必要があり、DRY 違反の温床でした。複数フィールド対応により、共通の検証ロジックを 1 箇所に集約できます。さらに `data_key` を受け取れることで、エラーメッセージを処理中のフィールド名で動的に組み立てられ、「どこで落ちたか」が利用者に伝わりやすくなります。重複の削減と情報量の増加を同時に達成する、よく考えられた拡張です。

> ⚠️ **シグネチャ変更に注意**：3.x の `def v(self, value):` は、4.x では `def v(self, value, data_key):` に変わります。**既存の `@validates` メソッドはすべて引数 `data_key` を追加する**必要があります。これも grep で網羅的に洗い出してください。

---

## **9. カスタムフィールドのジェネリック化と `_bind_to_schema` の引数名**

独自フィールドを定義している場合、2 つの変更があります。

### **9-1. カスタムフィールドは `fields.Field[T]` でジェネリックに**

カスタムフィールドは、デシリアライズ後の型 `T` をジェネリックパラメータとして指定するようになりました。

```python
# ❌ 3.x（旧）
from marshmallow import fields


class PositiveInt(fields.Field):
    def _deserialize(self, value, attr, data, **kwargs):
        return int(value)
```

```python
# ✅ 4.x（新）：デシリアライズ後の型を型引数で明示する
from marshmallow import fields


class PositiveInt(fields.Field[int]):   # → load 後の型は int
    def _deserialize(self, value, attr, data, **kwargs) -> int:
        return int(value)
```

### **9-2. `_bind_to_schema` の第 2 引数 `schema` → `parent`**

スキーマへのバインド時に呼ばれる `_bind_to_schema` の引数名が `schema` から **`parent`** に変わりました。フィールドは別のフィールド（`List` や `Nested`）の内側にもバインドされ得るため、「親」を指す `parent` の方が正確です。

```python
# ❌ 3.x（旧）
class MyField(fields.Field):
    def _bind_to_schema(self, field_name, schema):
        super()._bind_to_schema(field_name, schema)
        # schema を参照する処理...
```

```python
# ✅ 4.x（新）：引数名が parent に（バインド先はスキーマとは限らないため）
class MyField(fields.Field[str]):
    def _bind_to_schema(self, field_name, parent):
        super()._bind_to_schema(field_name, parent)
        # parent を参照する処理...
```

**なぜこれが優れているのか？**
`fields.Field[T]` のジェネリック化は、**型チェッカー（mypy / pyright）がカスタムフィールドの戻り型を理解できる**ことを意味します。`load()` の結果が `Any` ではなく `int` として推論され、IDE 補完と静的解析が効きます。`_bind_to_schema` の `parent` への改名は、バインド先が必ずしもトップレベルの `Schema` ではない（`fields.List` の中など）という**実態に名前を合わせた**もので、誤解を招く命名の是正です。

---

## **10. 標準ライブラリ化された処理：日付ユーティリティと `TimeDelta`**

marshmallow が内部に抱えていた一部のユーティリティが削除され、**Python 標準ライブラリ**を直接使う方針になりました。

### **10-1. 日付ユーティリティの削除**

`marshmallow.utils.from_iso_date` のような独自の日付パーサ／フォーマッタは削除されました。標準ライブラリの同等機能に置き換えます。

```python
# ❌ 3.x（旧）：marshmallow の内部ユーティリティを呼んでいた
from marshmallow.utils import from_iso_date

d = from_iso_date("2026-06-26")
```

```python
# ✅ 4.x（新）：標準ライブラリを使う
from datetime import date
from email.utils import parsedate_to_datetime

d = date.fromisoformat("2026-06-26")     # ISO 日付のパース
s = d.isoformat()                        # ISO 文字列へ
dt = parsedate_to_datetime("Wed, 26 Jun 2026 12:00:00 GMT")  # RFC 822 形式
```

### **10-2. `TimeDelta` はマイクロ秒を保持／`serialization_type` 削除**

`fields.TimeDelta` は、`float` から変換する際に**マイクロ秒精度を保持**するよう挙動が変わり、`serialization_type` 引数が削除されました。

```python
# ✅ 4.x：TimeDelta は精度を保ったまま timedelta を扱う
from datetime import timedelta
from marshmallow import Schema, fields


class JobSchema(Schema):
    # precision で出力単位を指定（serialization_type は廃止）
    duration = fields.TimeDelta(precision="seconds")


# dump/load でマイクロ秒以下が丸められず保持される
JobSchema().load({"duration": 1.5})   # → {'duration': timedelta(seconds=1, microseconds=500000)}
```

**なぜこれが優れているのか？**
日付処理を標準ライブラリへ寄せることは、marshmallow の**依存とメンテナンス範囲を縮小**します。`date.fromisoformat` は CPython 本体がメンテナンスする枯れた実装であり、ライブラリ独自実装より信頼性が高く、Python のバージョンアップとともに改善されます。「自前で持つより、標準にあるものを使う」というのは、コスト効率と信頼性の両面で正しい判断です。`TimeDelta` の精度保持は、丸め誤差による「1.5 秒が 1 秒になる」類のデータ欠損を防ぎます。

> 💡 **Python のバージョン**：4.x はやや新しめの Python を要求します。3.x からの移行と同時に、サポートの切れた古い Python ランタイムを使っていないかも確認しておくと、依存解決の衝突を避けられます。

---

## **11. 早見表：grep して機械的に置換する**

ここまでの全変更を 1 枚にまとめます。移行作業はこの表を「チェックリスト」として、各行を grep で洗い出しながら潰していきます。

| 3.x（旧） | 4.x（新） | 種別 |
| --- | --- | --- |
| `fields.Str(missing=...)` | `fields.Str(load_default=...)` | 入力時デフォルト |
| `fields.Int(default=...)` | `fields.Int(dump_default=...)` | 出力時デフォルト |
| `fields.Number()` / `fields.Mapping()` / `fields.Field()` の直接利用 | `fields.Integer()` / `Float()` / `Decimal()` / `fields.Dict()` | 抽象基底クラスのインスタンス化禁止 |
| `validate=` が `False` を返す | `ValidationError` を `raise` する | バリデータの返り値 |
| `@post_dump(pass_many=True)` | `@post_dump(pass_collection=True)` | デコレータ引数名 |
| `class Meta: fields = (...)` / `additional` | フィールドを明示的に宣言 | 暗黙のフィールド生成廃止 |
| `schema.context = {...}` | `contextvars.ContextVar` / `experimental.Context` | コンテキスト受け渡し |
| `@validates("name")` を個別に複数定義 | `@validates("name", "nickname")` + メソッドが `data_key` を受け取る | 複数フィールド対応 |
| `class MyField(fields.Field)` | `class MyField(fields.Field[T])` | カスタムフィールドのジェネリック化 |
| `_bind_to_schema(self, field_name, schema)` | `_bind_to_schema(self, field_name, parent)` | カスタムフィールドの引数名 |
| `marshmallow.utils.from_iso_date` 等 | 標準ライブラリ（`date.fromisoformat` 等） | 日付ユーティリティ削除 |
| `fields.TimeDelta(serialization_type=...)` | `fields.TimeDelta(precision=...)`（マイクロ秒保持） | TimeDelta の仕様変更 |

### **洗い出し用の grep パターン**

リポジトリ全体に対して、以下のパターンで該当箇所を機械的に列挙します。

```bash
# 最頻出：missing / default（ヒット件数が最も多い）
grep -rn "missing=" --include="*.py" .
grep -rn "default=" --include="*.py" .

# デコレータ引数の改名
grep -rn "pass_many=" --include="*.py" .

# 抽象基底クラスの直接利用
grep -rn "fields\.Number(" --include="*.py" .
grep -rn "fields\.Mapping(" --include="*.py" .
grep -rn "fields\.Field(" --include="*.py" .

# コンテキスト受け渡し（Schema.context の参照・代入）
grep -rn "\.context" --include="*.py" .

# 暗黙フィールド生成・カスタムフィールド
grep -rn "additional" --include="*.py" .
grep -rn "_bind_to_schema" --include="*.py" .
```

> ⚠️ **`default=` と `missing=` は誤検出が多い**：`grep "default="` は marshmallow と無関係なコード（`dict.get` の引数、関数のデフォルト引数など）も大量にヒットします。**機械的に sed で一括置換してはいけません**。ヒットした各行が「marshmallow のフィールド定義か」を目視で確認し、第 2 章の区別（load 用か dump 用か）に従って 1 つずつ置換してください。grep は「見つける」ための道具であり、「置き換える」のは人間の判断です。

---

## **12. テストで退行を止める：往復のゴールデンテスト**

移行で最も怖いのは、**型シグネチャは通るのに出力 JSON の中身が静かに変わる**退行です。`dump_default` の付け忘れ、`pass_collection` の漏れ、`context` 移行のミス——これらはエラーを出さず、本番のレスポンスだけが変わります。これを止める唯一の方法は、**移行前の入出力をテストで固定（ゴールデン化）すること**です。

```python
# tests/test_user_schema.py
import pytest
from app.schemas import UserSchema


def test_dump_golden():
    """dump の出力を「既知の正解」として固定する（移行で中身が変われば落ちる）"""
    user = {"name": "友田", "email": "tomoda@example.com", "role": "member"}
    result = UserSchema().dump(user)
    assert result == {
        "name": "友田",
        "email": "tomoda@example.com",
        "role": "member",
    }


def test_load_roundtrip():
    """load → dump の往復で値が保存されることを確認する"""
    payload = {"name": "友田", "email": "tomoda@example.com"}
    loaded = UserSchema().load(payload)
    dumped = UserSchema().dump(loaded)
    # 往復しても入力した値が失われない
    assert dumped["name"] == payload["name"]
    assert dumped["email"] == payload["email"]


def test_load_rejects_invalid_email():
    """検証失敗の挙動も固定する（移行でエラーメッセージ構造が変わっていないか）"""
    from marshmallow import ValidationError

    with pytest.raises(ValidationError) as exc:
        UserSchema().load({"name": "友田", "email": "not-an-email"})
    assert "email" in exc.value.messages
```

### **CI で「バージョンを上げる PR」を緑にする**

移行は 1 つの PR で完結させ、**その PR の CI が緑になることを「移行完了」の定義**にします。理想は、依存を上げる差分とコード修正を同じ PR に含め、テストスイートが全部通ることをマージ条件にすることです。

```yaml
# .github/workflows/test.yml（要点のみ）
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.13"
      - run: pip install -r requirements.txt   # marshmallow>=4,<5 を固定済み
      # 非推奨警告をエラー化し、移行漏れを検出する
      - run: python -W error::DeprecationWarning -m pytest
```

**なぜこれが優れているのか？**
ゴールデンテストは、「型は通るが意味が変わった」という、**移行で最も見つけにくいクラスのバグを機械的に検出**します。CLAUDE.md の「検証パスを実装前に用意する」という原則のとおり、**移行作業そのものよりも先に**、現状の入出力をテストで固定しておくのが理想です。固定された期待値があれば、移行は「テストを赤から緑に戻す作業」という、ゴールの明確なタスクに変わります。これが「機械的置換 + テストで担保」という本記事の主題の核心です。

---

## **13. よくある落とし穴**

移行で実際につまずきやすいポイントを整理します。

### **`load_default` と `dump_default` の意味の取り違え**

最頻出かつ最も静かなバグです。「`missing` は load 用、`default` は dump 用」という 3.x の対応を覚えていないと、置換時に逆を当ててしまいます。**`load_default` = 入力が欠けたときに補う値、`dump_default` = 出力が欠けたときに補う値**。grep でヒットした各箇所が `missing` 由来（→ `load_default`）か `default` 由来（→ `dump_default`）かを、必ず元の引数名で判断してください。

### **`@validates` のシグネチャ変更漏れ**

`def v(self, value):` のままだと、4.x で `data_key` が渡された瞬間に `TypeError` になります。`@validates` を使っている全メソッドに `data_key` 引数を追加したか、第 8 章の grep で確認します。

### **サードパーティ・エコシステムの対応バージョン確認**

marshmallow は単体で使われることは少なく、周辺ライブラリと組み合わさっています。**それらが marshmallow 4 に対応したバージョンを出しているか**を、上げる前に必ず確認してください。

| ライブラリ | 確認すること |
| --- | --- |
| `marshmallow-sqlalchemy` | marshmallow 4 対応バージョンか（`SQLAlchemyAutoSchema` を使っている場合は必須） |
| `flask-smorest` | marshmallow 4 を要求／許容するバージョンか（API 全体のスキーマ基盤） |
| `apispec` | OpenAPI 生成が marshmallow 4 のフィールドに追従しているか |
| `webargs` | リクエストパースで marshmallow 4 を受け付けるか |

これらのいずれかが marshmallow 4 に未対応だと、**marshmallow 単体は上がってもアプリ全体がインストール時に依存解決で失敗**します。移行を始める前に、`pip install "marshmallow>=4"` を**ドライランや別環境で試し**、依存の衝突がないかを先に確かめるのが安全です。

### **ネストスキーマの連鎖的な影響**

`fields.Nested` で深く入れ子になったスキーマは、**子スキーマの変更が親の出力に波及**します。1 つのスキーマを直したら、それをネストしている親スキーマのゴールデンテストも走らせ、出力が変わっていないことを確認してください。

---

## **結論：恐れず、しかし機械的に、テストを盾にして上げる**

marshmallow 3 → 4 の移行は、未知の難事ではなく、**正体の分かった有限個の置換作業**です。本記事の要点を再掲します。

1. **戦略は 4 ステップ**——①バージョン固定 → ②3.x で `DeprecationWarning` をエラー化して潰す → ③4.x へ上げて対応表で置換 → ④テストで担保。各ステップを小さな PR に分ける。
2. **最頻出は `missing`/`default` → `load_default`/`dump_default` と `pass_many` → `pass_collection`**。grep で全箇所を洗い出すが、置換は人間が 1 つずつ判断する。
3. **バリデータは `False` を返す方式が廃止**され、必ず `ValidationError` を `raise` する方式に統一された（戻り値忘れのバグを根絶する安全な変更）。
4. **`Schema.context` は廃止**され、`contextvars.ContextVar` / `experimental.Context` へ移行する（共有可変状態を排し、`async`/スレッドで安全に）。
5. **抽象基底クラス（`Number`/`Mapping`/`Field`）の直接利用が禁止**され、暗黙フィールド生成も廃止——型と意図をスキーマに明示することが強制される。
6. **退行はテストで止める**。移行前に load/dump の往復をゴールデン化し、CI でバージョンアップ PR を緑にして初めて「完了」とする。

メジャーバージョンアップを先送りにし続けることは、それ自体が静かに膨らむ技術的負債です。**対応表と grep で機械的に洗い出し、ゴールデンテストを盾にして小さく上げる**——この規律があれば、移行は「いつか来る大仕事」ではなく「今週終わらせられる作業」になります。各 API の意味で迷ったら、土台となる [marshmallow 実践ガイド](/blog/marshmallow-python-serialization-validation-production-guide) に立ち返ってください。

公式の一次情報も、移行作業の傍らに開いておくことを強くお勧めします。

- [Upgrading to newer releases（3→4 移行の正準ガイド）](https://marshmallow.readthedocs.io/en/stable/upgrading.html)
- [Changelog](https://marshmallow.readthedocs.io/en/stable/changelog.html)
- [marshmallow 公式ドキュメント](https://marshmallow.readthedocs.io/en/stable/)

---

### **型安全なバックエンド設計のご相談**

筆者は、経済産業大臣賞を受賞した B2B SaaS のバックエンドを **Python / Flask / SQLAlchemy / PostgreSQL** で設計・運用し、その境界バリデーションを marshmallow で実装してきました。ライブラリのメジャーバージョンアップは、放置すれば技術的負債、計画的に行えば品質改善の好機です。**破壊的変更の影響範囲の調査、ゴールデンテストによる退行検出網の整備、段階的な移行 PR の設計と CI 整備**まで、生成 AI を活用して高速かつ安全に伴走します。marshmallow を含む Python バックエンドのバージョンアップ・型安全化・リファクタリングについて、お気軽にご相談ください。
