# marshmallow 実践ガイド：Python オブジェクトのシリアライズ／検証を境界で堅牢に設計する（v4対応）

> marshmallow公式ドキュメント（v4.3）に忠実に、Schema/fieldsの双方向シリアライズ、load()による境界バリデーション、@validates/@validates_schema、Nested、load_only/dump_onlyの安全設計、marshmallow-sqlalchemy連携、3→4移行、Pydanticとの使い分けまでを実務観点で解説します。

- 公開日: 2026-06-26
- 著者: 友田 陽大
- タグ: Python, marshmallow, シリアライズ, バリデーション, 型安全, Flask, SQLAlchemy, アーキテクチャ設計
- URL: https://tomodahinata.com/blog/marshmallow-python-serialization-validation-production-guide

## 要点

- marshmallow は「Python オブジェクト ⇄ dict/JSON」の双方向変換を Schema として宣言し、load() で境界の不正データを ValidationError として堰き止める
- load_only / dump_only / unknown=RAISE を使い、パスワード漏洩・マスアサインメント（不正な権限昇格）といったセキュリティ事故をスキーマの構造で防ぐ
- 検証は fields の validate=、単一・複数フィールドの @validates、フィールド間の @validates_schema という三層で責務を分けて設計する
- @post_load でドメインオブジェクトへ写像し、fields.Nested / Pluck で複合データを組み立て、marshmallow-sqlalchemy で ORM と型安全に接続する
- marshmallow 3 → 4 では missing/default → load_default/dump_default など破壊的変更があり、本記事の早見表で機械的に移行できる

---

## **導入：なぜ今あえて marshmallow なのか**

堅牢なバックエンドの設計思想は、一文に集約できます——**「システム境界の外から来るデータを、決して信頼しない」**。HTTP リクエストボディ、外部 API のレスポンス、フォーム入力、メッセージキューのペイロード。これらはすべて「型の保証がない、検証されていないデータ」であり、アプリケーションの内側に素通しさせた瞬間、`KeyError` や `AttributeError`、そして最悪の場合は**マスアサインメント（不正な権限昇格）やデータ漏洩**へと姿を変えます。

**marshmallow は、この境界に立つ「双方向の門番」**です。Pydantic が「型ファーストのモデル」なら、marshmallow は **ORM／フレームワークに依存しない、シリアライズ／デシリアライズの専用ライブラリ**として 2013 年から成熟を重ねてきました。Flask + SQLAlchemy のスタックでは、いまも事実上の標準です。その本質は、たった 2 つの方向に集約されます。

- **`load()`（デシリアライズ）**：信頼できない外部入力（dict / JSON）を**検証し、正規化して**、内部で扱える形に変換する。検証に失敗すれば `ValidationError` で堰き止める。
- **`dump()`（シリアライズ）**：内部の Python オブジェクト（ORM モデルなど）を、**外向きの安全な表現（dict / JSON）へ整形する**。

筆者は、経済産業大臣賞を受賞した B2B SaaS のバックエンドを **Python / Flask / SQLAlchemy / PostgreSQL** で設計・実装し、`Router → UseCase → Repository → Model` の厳格な層分離で本番運用してきました。そのプロジェクトの境界バリデーションを担っていたのが、まさに **marshmallow** です。本記事は、その実戦知見を、**最新の marshmallow 公式ドキュメント（[marshmallow.readthedocs.io](https://marshmallow.readthedocs.io/en/stable/)）に忠実でありながら、それより一段わかりやすく**整理したものです。

> 💡 **この記事で扱うバージョン**：marshmallow **4.3.0**（2026年4月時点の安定版）を前提とします。Web 上の記事や生成 AI が出力するコードの多くは、まだ **3.x 系のレガシースタイル**（`missing=` / `default=` / `Schema.context`）で書かれています。本記事は v4 の正準スタイルで書き、章末に **3 → 4 移行の早見表**を用意しました。

> 💡 この記事は Python バックエンド設計の連作の一部です。型ファーストの対になる選択肢は [Pydantic v2 実践ガイド](/blog/pydantic-v2-production-validation-type-safety)、Web フレームワーク層は [FastAPI 本番運用ガイド](/blog/fastapi-production-async-pydantic-observability-guide)、永続化層は [SQLAlchemy 2.0 実践ガイド](/blog/sqlalchemy-2-typed-orm-production-guide) を併せて読むと、境界から DB までの一貫した設計が見渡せます。

---

## **1. `Schema` と `fields`：双方向の変換を宣言的に定義する**

marshmallow の出発点は `Schema` クラスの継承です。クラス属性に `fields` の記述子を並べるだけで、それが**検証・シリアライズ・デシリアライズの単一の真実（Single Source of Truth）**になります。

```python
from datetime import datetime
from marshmallow import Schema, fields


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


user = {"name": "友田", "email": "tomoda@example.com", "created_at": datetime.now()}
schema = UserSchema()

schema.dump(user)   # → dict:  {'name': '友田', 'email': 'tomoda@example.com', 'created_at': '2026-06-26T...'}
schema.dumps(user)  # → JSON 文字列:  '{"name": "友田", "email": "tomoda@example.com", ...}'
```

`dump()` は **Python の dict** を、`dumps()` は **JSON 文字列**を返します（末尾の `s` は string の `s`）。`datetime` のような JSON 非互換の型は、`fields.DateTime()` が ISO 8601 文字列へ自動変換します——これが「内部の型 → 外向きの表現」という `dump` の仕事です。

なお、クラス定義の代わりに `Schema.from_dict()` で**実行時にスキーマを生成**することもできます。設定値から動的にスキーマを組み立てたいときに有効です。

```python
UserSchema = Schema.from_dict(
    {"name": fields.Str(required=True), "email": fields.Email(required=True)}
)
```

### **コレクションは `many=True` で扱う**

複数オブジェクトの一括変換は `many=True` を指定します。スキーマ生成時でも、`dump()`/`load()` 呼び出し時でも指定できます。

```python
UserSchema(many=True).dump(users)      # ① スキーマ単位
UserSchema().dump(users, many=True)    # ② 呼び出し単位
```

**なぜこれが優れているのか？**
「どの項目を・どんな型で・どう変換して入出力するか」という知識が、`Schema` という**一箇所**に凝集します。レスポンス整形のロジックがビュー関数やサービス層に散らばらないため、出力仕様が変わっても修正箇所はスキーマ定義に局所化されます。これは CLAUDE.md でいう DRY と ETC（Easy To Change）の実践です。

---

## **2. `load()`：境界の不正データを検証して堰き止める**

`dump()` の逆が `load()` です。**信頼できない外部入力を検証し、正規化された dict を返す**——失敗すれば `ValidationError` を送出します。これが marshmallow の最重要機能です。

```python
from marshmallow import ValidationError

try:
    data = UserSchema().load({"name": "友田", "email": "not-an-email"})
except ValidationError as err:
    print(err.messages)    # {'email': ['Not a valid email address.']}
    print(err.valid_data)  # {'name': '友田'}  ← 検証を通った分だけ取り出せる
```

`ValidationError` は 2 つの情報を持ちます。

- **`err.messages`**：フィールド名をキーにしたエラーメッセージの dict。そのまま 422 レスポンスのボディにできます。
- **`err.valid_data`**：検証を通過したフィールドだけを含む dict。部分的な処理に使えます。

`many=True` のときは、エラーが**要素のインデックスをキー**として返るため、「配列の何番目の、どのフィールドが、なぜ落ちたか」が一目で分かります。

```python
# {1: {'email': ['Not a valid email address.']},
#  3: {'name': ['Missing data for required field.']}}
```

### **`required` / `allow_none` / `load_default` / `dump_default`**

入力の必須・任意・デフォルトは、フィールドの引数で宣言します。**v4 では `missing=`／`default=` は廃止され、`load_default`（入力時のデフォルト）／`dump_default`（出力時のデフォルト）に統一**されました。「入力と出力で意味が違う」ことが名前から明確になる、優れた変更です。

```python
import uuid
from datetime import datetime
from marshmallow import Schema, fields


class AccountSchema(Schema):
    # required=True：欠落したら "Missing data for required field." で落とす
    email = fields.Email(required=True, error_messages={"required": "メールアドレスは必須です。"})

    # 明示的に None を許容する（デフォルトでは None は不許可）
    nickname = fields.Str(allow_none=True)

    # load 時に値が無ければこのデフォルトを補う（呼び出し可能オブジェクトも可）
    id = fields.UUID(load_default=uuid.uuid4)

    # dump 時に値が無ければこのデフォルトで出力する
    role = fields.Str(dump_default="member")
```

### **`data_key`：外部の命名と内部の命名を分離する**

外部 API が `camelCase`、内部コードは `snake_case`——よくある衝突です。`data_key` がこの翻訳を境界に閉じ込めます。

```python
class UserSchema(Schema):
    user_name = fields.Str(data_key="userName")
    email = fields.Email(data_key="emailAddress")

# load:  {"userName": "友田", "emailAddress": "a@b.com"} → {'user_name': '友田', 'email': 'a@b.com'}
# dump:  内部の snake_case → 外向きの camelCase へ戻る
```

### **`partial`：PATCH のための部分検証**

更新系 API では「送られたフィールドだけ検証したい」場面があります。`partial=True` で全 `required` を、`partial=("name",)` で特定フィールドの `required` をスキップできます。

```python
UserSchema().load({"email": "a@b.com"}, partial=("name",))  # name の必須を免除
```

**なぜこれが優れているのか？**
「検証して初めて内部の型になる」という規律が、`load()` という関数呼び出しの形でコードに現れます。境界を通過したデータは「検証済み」であることがコード上で保証され、下流のあらゆる処理がその前提を信頼できます。`if not data.get("email"): ...` のような**手書きの検証 if 文がビジネスロジックに混入するのを防ぎ**、SRP（単一責任）を守ります。

---

## **3. セキュリティ境界としての Schema：3 つの安全装置**

marshmallow の真価は、**「何を内側に入れ、何を外側に出さないか」をスキーマの構造で制御できる**点にあります。これは OWASP が警告する典型的な脆弱性を、運用の注意深さではなく**コードの構造**で防ぐということです。

### **① `dump_only`：クライアントに「書かせない」フィールド**

`id` / `created_at` / `role` のような**サーバーが決めるべき値**を、`request.json` から無防備に受け取ると、攻撃者が `{"role": "admin"}` を送り込む**マスアサインメント脆弱性**になります。`dump_only=True` を付けたフィールドは**出力専用**となり、`load()` では完全に無視されます。

```python
class UserSchema(Schema):
    id = fields.Int(dump_only=True)              # 出力のみ。load では絶対に書き込めない
    role = fields.Str(dump_only=True)            # 権限はサーバーが決定する
    created_at = fields.DateTime(dump_only=True)
    email = fields.Email(required=True)          # これは load で受け付ける
```

### **② `load_only`：レスポンスに「絶対に出さない」フィールド**

パスワードやトークンを誤ってレスポンスに含めるのは典型的な情報漏洩事故です。`load_only=True` を付けたフィールドは**入力専用**となり、`dump()` の出力には決して含まれません。

```python
class SignupSchema(Schema):
    email = fields.Email(required=True)
    password = fields.Str(load_only=True, required=True, validate=validate.Length(min=12))
    # password は load では受け取るが、dump では出力されない → 漏洩を構造的に防ぐ
```

### **③ `unknown=RAISE`：未知のキーを拒否する（デフォルト）**

marshmallow は**デフォルトで未知のフィールドを `ValidationError` で拒否**します（`unknown=RAISE`）。これは `Model(**request.json)` のような無防備な展開とは対極の、安全側に倒れた設計です。挙動は明示的に切り替えられます。

```python
from marshmallow import Schema, fields, EXCLUDE, INCLUDE, RAISE


class StrictSchema(Schema):
    class Meta:
        unknown = RAISE   # デフォルト：未知のキーはエラー（最も安全）
        # EXCLUDE → 未知のキーを黙って捨てる / INCLUDE → そのまま通す

    name = fields.Str()


# 呼び出し単位での上書きも可能
StrictSchema().load(payload, unknown=EXCLUDE)
```

| オプション | 未知のキーの扱い | 主な用途 |
| --- | --- | --- |
| `RAISE`（デフォルト） | `ValidationError` を送出 | 厳格な入力検証（第一選択） |
| `EXCLUDE` | 黙って破棄 | 外部 API の余分なキーを無視したいとき |
| `INCLUDE` | 検証せず通す | スキーマレスな項目を素通しするとき（要注意） |

**なぜこれが優れているのか？**
セキュリティを「レビューでの気づき」に頼ると、必ずいつか漏れます。`dump_only` / `load_only` / `unknown=RAISE` は、**スキーマ定義そのものに安全制約を宣言**することで、「うっかり書ける／うっかり出る」を**構造的に不可能**にします。これは CLAUDE.md のセキュリティ原則「すべての外部入力を境界で検証・サニタイズし、最小権限を適用する」の具体的な実装です。

---

## **4. バリデーション：`fields` → `@validates` → `@validates_schema` の三層**

型の検証だけでは「18 歳以上」「終了日時は開始日時より後」といった**業務ルール**は表現できません。marshmallow は検証を**責務に応じた三層**で提供します。

### **第一層：`validate=` 引数（単一フィールドの静的ルール）**

最も軽量な検証は、`validate=` に `marshmallow.validate` のバリデータを渡す方法です。

```python
from marshmallow import Schema, fields, validate


class UserSchema(Schema):
    name = fields.Str(validate=validate.Length(min=1, max=120))
    age = fields.Int(validate=validate.Range(min=18, max=120))
    permission = fields.Str(validate=validate.OneOf(["read", "write", "admin"]))
    sku = fields.Str(validate=validate.Regexp(r"^[A-Z]{3}-\d{4}$"))
    # 複数のバリデータはリストで合成できる
    slug = fields.Str(validate=[validate.Length(min=3), validate.Regexp(r"^[a-z0-9-]+$")])
```

主な組み込みバリデータは公式どおり `Length` / `Range` / `OneOf` / `Regexp` / `Email` / `Equal` / `ContainsOnly` などです。

> ⚠️ **v4 の破壊的変更**：3.x では `validate=lambda x: x == "ok"` のように **`False` を返す**関数も使えましたが、**v4 ではバリデータは必ず `ValidationError` を `raise` しなければなりません**。`False` を返す書き方は動かなくなります。

### **第二層：`@validates`（フィールド単位のカスタム検証）**

組み込みでは表現できないロジックは `@validates` デコレータで書きます。**v4 では複数フィールド名を渡せ、メソッドは `data_key` を受け取ります**。

```python
from marshmallow import Schema, fields, validates, ValidationError


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

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

### **第三層：`@validates_schema`（フィールド間の関係検証）**

「終了日時は開始日時より後」「割引後価格は定価以下」のように**複数フィールドにまたがる不変条件**は `@validates_schema` に集約します。

```python
from marshmallow import Schema, fields, validates_schema, ValidationError


class BookingSchema(Schema):
    start_at = fields.DateTime(required=True)
    end_at = fields.DateTime(required=True)

    @validates_schema
    def validate_period(self, data, **kwargs):
        # ここに来る時点で start_at / end_at は型検証済み（後述の skip_on_field_errors）
        if data["start_at"] >= data["end_at"]:
            # 第2引数でエラーを特定フィールドに紐付けられる
            raise ValidationError("終了日時は開始日時より後にしてください。", "end_at")
```

複数フィールドにエラーを割り当てたいときは、**フィールド名をキーにした dict** を渡します。

```python
    @validates_schema
    def validate_bounds(self, data, **kwargs):
        errors = {}
        if data["field_b"] <= data["field_a"]:
            errors["field_b"] = ["field_b は field_a より大きくしてください。"]
        if errors:
            raise ValidationError(errors)
```

> 💡 **`skip_on_field_errors` の既定は `True`**：`@validates_schema` は、**個々のフィールド検証が既に失敗している場合はスキップ**されます（v3.0 以降の既定）。これにより、`data["start_at"]` が存在しない（型検証で落ちている）のに schema バリデータが `KeyError` を起こす、という事故を防げます。フィールド名を持たないスキーマ全体のエラーは、`err.messages` の **`_schema` キー**に格納されます。

**なぜこの三層分割が優れているのか？**
検証ロジックを「とりあえずサービス層の if 文」に書くと、ビジネスロジックと検証が混ざり、テストも再利用も困難になります。marshmallow の三層は、**静的ルールは `validate=`、フィールド固有のロジックは `@validates`、フィールド間の不変条件は `@validates_schema`** と責務を明確に分けます。「`BookingSchema` のインスタンスが存在する＝期間が正しい」という保証がスキーマ内で完結し、下流コードがその前提を信頼できます（SRP の徹底）。

---

## **5. 前処理・後処理：`@pre_load` / `@post_load` でドメインへ写像する**

検証の前後にフックを差し込めるのが、marshmallow のもう一つの強みです。処理は **`@pre_load` → フィールド検証 → `@post_load`** の順に流れます。

### **`@pre_load`：検証前に入力を正規化する**

「前後の空白を除去」「メールを小文字化」といった**正規化は検証より前**に済ませるべきです。

```python
from marshmallow import Schema, fields, pre_load


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

    @pre_load
    def normalize(self, data, **kwargs):
        if isinstance(data.get("email"), str):
            data["email"] = data["email"].strip().lower()
        return data
```

### **`@post_load`：検証済み dict をドメインオブジェクトへ**

`load()` は既定で dict を返しますが、`@post_load` を使えば**検証済みデータを自分のドメインクラスのインスタンスに変換**できます。これが「境界を越えたら、もう dict ではなく型付きオブジェクト」という設計を実現します。

```python
from dataclasses import dataclass
from marshmallow import Schema, fields, post_load


@dataclass
class User:
    name: str
    email: str


class UserSchema(Schema):
    name = fields.Str(required=True)
    email = fields.Email(required=True)

    @post_load
    def make_user(self, data, **kwargs):
        return User(**data)


UserSchema().load({"name": "友田", "email": "a@b.com"})  # → User(name='友田', email='a@b.com')
```

### **`@post_dump`：出力をエンベロープで包む**

API レスポンスを `{"result": ...}` / `{"results": [...]}` のような共通の封筒で包みたいときは `@post_dump` を使います。`many` を受け取れるよう、**v4 では `pass_collection=True`**（3.x の `pass_many` から改名）を指定します。

```python
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}
```

> 💡 **最新版のショートカット（4.3.0）**：marshmallow 4.3.0 では `fields.Field` に **`pre_load` / `post_load` 引数**が追加され、フィールド単位の前後処理をデコレータなしで宣言できるようになりました。スキーマ全体ではなく特定フィールドだけを整形したいケースで有用です（詳細は[公式チェンジログ](https://marshmallow.readthedocs.io/en/stable/changelog.html)を参照）。

---

## **6. ネスト：複合データ構造を組み立てる**

実務のデータは平坦ではありません。`fields.Nested` で**スキーマを入れ子**にできます。

```python
class AuthorSchema(Schema):
    id = fields.Int(dump_only=True)
    name = fields.Str(required=True)


class BookSchema(Schema):
    title = fields.Str(required=True)
    author = fields.Nested(AuthorSchema)                       # 単一のネスト
    reviewers = fields.List(fields.Nested(AuthorSchema))       # ネストのコレクション
```

### **`only` / `exclude`：ネストの一部だけを使う**

一覧 API では「著者は名前だけ」で十分なことが多い。ネストの一部だけを `only` / `exclude` で絞り込めます。**ドット記法で多階層**も指定できます。

```python
class BookListSchema(Schema):
    title = fields.Str()
    author = fields.Nested(AuthorSchema(only=("name",)))       # 著者は名前だけ


class SiteSchema(Schema):
    book = fields.Nested(BookListSchema)

# 2階層下のフィールドだけを抜き出す
SiteSchema(only=("book.author.name",)).dump(site)
```

### **`fields.Pluck`：ネストを 1 属性に平坦化する**

「著者オブジェクトではなく、著者名の配列だけが欲しい」場合は `fields.Pluck` が最短です。

```python
class BookSchema(Schema):
    title = fields.Str()
    author = fields.Pluck(AuthorSchema, "name")   # → {"title": "...", "author": "友田"}
```

### **循環参照・自己参照は `lambda` で解決する**

相互に参照し合うスキーマ（著者 ⇄ 書籍）や自己参照（社員 → 上司）は、**`lambda` で遅延評価**して定義順・循環の問題を回避します。文字列でクラス名を渡す方法もあり、循環インポートの回避に有効です。

```python
class UserSchema(Schema):
    name = fields.Str()
    # 自己参照：employer から先は employer を畳んで無限再帰を防ぐ
    employer = fields.Nested(lambda: UserSchema(exclude=("employer",)))
```

**なぜこれが優れているのか？**
同じ `AuthorSchema` という**単一の定義**から、`only` / `exclude` / `Pluck` を切り替えるだけで「詳細ビュー」「一覧ビュー」「埋め込みビュー」を作り分けられます。ビューごとにモデルを増やす必要がなく、定義の重複（DRY 違反）を避けられます。これは marshmallow が「プレゼンテーション層のシリアライズ」に強い、と言われる理由の核心です。

---

## **7. 実戦応用：`marshmallow-sqlalchemy` で ORM と接続する**

Flask + SQLAlchemy のスタックでは、`marshmallow-sqlalchemy` を使うと **SQLAlchemy モデルからスキーマを自動生成**でき、ボイラープレートが激減します。

```python
from marshmallow_sqlalchemy import SQLAlchemyAutoSchema, auto_field


class AuthorSchema(SQLAlchemyAutoSchema):
    class Meta:
        model = Author              # このモデルの列からフィールドを自動生成
        load_instance = True        # load() が dict ではなく Author インスタンスを返す
        include_relationships = True # リレーションも出力に含める
        include_fk = True           # 外部キー列も含める

    # 自動生成を上書きしたい列だけ auto_field で個別宣言できる
    email = auto_field(required=True)
```

`load_instance = True` が肝です。`@post_load` を自前で書かずとも、**`load()` が検証済みの ORM インスタンスを返す**ため、そのまま `session.add()` できます。

### **Flask のエンドポイント：1 つのスキーマで「入口」と「出口」を守る**

ここまでの要素を組み合わせると、API エンドポイントは驚くほど簡潔かつ堅牢になります。**`load()` が入力境界を、`dump()` が出力境界を**担い、検証エラーは 422 で返します。

```python
@app.post("/authors")
def create_author():
    schema = AuthorSchema()
    try:
        # ① 入口：未知キー拒否・型検証・業務ルール検証をすべて通過した ORM インスタンスだけが残る
        author = schema.load(request.get_json(), session=db.session)
    except ValidationError as err:
        # ② エラーは構造化されたまま 422 で返す（err.messages はフィールド名→メッセージの dict）
        return jsonify(errors=err.messages), 422

    db.session.add(author)
    db.session.commit()

    # ③ 出口：dump_only / load_only により、安全に整形された表現だけが外へ出る
    return jsonify(schema.dump(author)), 201
```

**なぜこれが優れているのか？**
ビュー関数から、検証・型変換・整形のすべてが消え、残るのは「保存する」という本質的な処理だけです。入力の安全性（マスアサインメント防止）と出力の安全性（機密漏洩防止）は、**スキーマ定義という宣言**に委譲されています。これは「I/O・検証・ビジネスロジックを層で分離する」という設計原則そのものです。

---

## **8. `marshmallow 3` → `4` 移行：本番で踏む変更点の早見表**

既存の 3.x コードや、生成 AI が出力しがちな 3.x スタイルに遭遇したら、次の対応表で機械的に置き換えられます。公式の[アップグレードガイド](https://marshmallow.readthedocs.io/en/stable/upgrading.html)に基づく主要な破壊的変更です。

| 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])` | カスタムフィールドのジェネリック化 |
| `marshmallow.utils.from_iso_date` 等 | 標準ライブラリ（`date.fromisoformat` 等） | 日付ユーティリティ削除 |
| `_bind_to_schema(self, field_name, schema)` | `_bind_to_schema(self, field_name, parent)` | カスタムフィールドの引数名 |

> 💡 **移行の勘所**：最頻出は **`missing` / `default` → `load_default` / `dump_default`** と **`pass_many` → `pass_collection`** です。`grep -rn "missing=\|default=\|pass_many=\|\.context" .` で機械的に洗い出すのが確実です。`fields.Number()` の直接利用は、意図した型（整数か小数か）を明示する好機でもあります。

---

## **9. `marshmallow` か `Pydantic` か：選定の軸**

両者はしばしば比較されますが、**排他ではなく、役割で選ぶもの**です。設計の起点が根本的に異なります。

| 観点 | marshmallow | Pydantic v2 |
| --- | --- | --- |
| スキーマ定義 | `Schema` クラス + `fields` 記述子（明示的） | 型アノテーション + `BaseModel`（型ファースト） |
| 主眼 | 双方向のシリアライズ／デシリアライズ（プレゼンテーション） | 型駆動のドメインモデル＆検証 |
| 速度 | 純 Python 実装 | Rust 製 `pydantic-core` で高速 |
| エコシステム | Flask / SQLAlchemy（`marshmallow-sqlalchemy`）が成熟 | FastAPI と一体・JSON Schema を標準出力 |
| 同一データの複数ビュー | `only` / `exclude` / `Pluck` で容易 | ビューごとに別モデルを定義しがち |
| 型チェッカー連携 | 記述子ベースでやや弱め | アノテーション直結で強力 |

**選定の指針**は明快です。

- **marshmallow を選ぶ**：既存の **Flask / SQLAlchemy** 資産がある、**同一データの複数表現（一覧／詳細／管理用）**を柔軟に作り分けたい、シリアライズ／検証ロジックを ORM モデルから明示的に分離したい。
- **Pydantic v2 を選ぶ**：**FastAPI** で新規構築する、**型ファースト**で IDE 補完と静的解析を最大化したい、高 QPS で**速度**が要件、JSON Schema を自動生成したい。

筆者は、Flask/SQLAlchemy で構築した受賞 B2B SaaS では marshmallow を、FastAPI ベースの新規プロジェクトでは Pydantic を採用しています。**「境界の外を信頼しない」という規律は両者で完全に同一**であり、どちらを選んでも本質は変わりません。Pydantic 側の設計は [Pydantic v2 実践ガイド](/blog/pydantic-v2-production-validation-type-safety) で詳説しています。

---

## **結論：シリアライズと検証を「境界の設計」に昇華する**

marshmallow は、ORM・フレームワークに依存しない、成熟したシリアライズ／デシリアライズライブラリです。本記事の要点を再掲します。

1. **`Schema` + `fields`** で双方向の変換を宣言し、`dump()` で出力、`load()` で**境界の入力を検証**する。
2. **`dump_only` / `load_only` / `unknown=RAISE`** で、マスアサインメントと機密漏洩を**スキーマの構造**で防ぐ。
3. 検証は **`validate=`（静的）→ `@validates`（フィールド固有）→ `@validates_schema`（フィールド間）** の三層で責務分離する。
4. **`@pre_load` で正規化、`@post_load` でドメインオブジェクト化**し、`fields.Nested` / `Pluck` で複合構造を組み立てる。
5. **`marshmallow-sqlalchemy`** で ORM と接続し、1 つのスキーマで API の入口と出口を同時に守る。
6. **`3 → 4` 移行**は `load_default` / `dump_default` / `pass_collection` への置換が中心。早見表で機械的に対応する。

「動くコード」と「10 年運用できるコード」の差は、**信頼できないデータをどこで・どう堰き止め、内部の値をどう安全に外へ出すか**という境界設計の積み重ねにあります。marshmallow は、その境界を宣言的なスキーマとして表現する、実績ある道具です。

さらなる探求として、公式ドキュメントの以下を本記事の設計観点を念頭に再読することをお勧めします。

- [Quickstart](https://marshmallow.readthedocs.io/en/stable/quickstart.html)
- [Nesting Schemas](https://marshmallow.readthedocs.io/en/stable/nesting.html)
- [Custom Fields](https://marshmallow.readthedocs.io/en/stable/custom_fields.html)
- [Extending Schemas（pre/post 処理・スキーマ検証）](https://marshmallow.readthedocs.io/en/stable/extending/schema_validation.html)
- [Upgrading to newer releases（3→4 移行）](https://marshmallow.readthedocs.io/en/stable/upgrading.html)
- [marshmallow-sqlalchemy](https://marshmallow-sqlalchemy.readthedocs.io/en/latest/)

---

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

筆者は、ここで解説した「システム境界で外部入力を必ず検証し、内部の値を安全に整形して返す」という規律を、経済産業大臣賞を受賞した B2B SaaS の本番環境で、marshmallow による境界バリデーションとして実装・運用してきました。FastAPI ベースのスタックでは、その役割を Pydantic v2 が担います。型安全な入力検証・レスポンス整形・マスアサインメント対策・ORM 連携の境界防御といった、**事業の信頼性に直結する基盤**を、生成 AI を活用して高速かつ高品質に構築します。Python を用いたバックエンド開発・既存システムの型安全化について、お気軽にご相談ください。
