# marshmallow カスタムフィールドと高度なバリデーション：再利用可能なドメイン型を設計する

> marshmallowのカスタムフィールドを公式仕様に忠実に解説。fields.Field[T]と_serialize/_deserialize、make_errorとerror_messagesのi18n、金額(Decimal)・電話(E.164)・列挙(fields.Enum)など再利用可能なドメイン型、fields.Method/Function/Constant、既存フィールド拡張までを実コードで示します。

- 公開日: 2026-06-26
- 著者: 友田 陽大
- タグ: Python, marshmallow, バリデーション, 型安全, ドメインモデリング, DRY, アーキテクチャ設計
- URL: https://tomodahinata.com/blog/marshmallow-custom-fields-advanced-validation-guide

## 要点

- fields.Field[T]を継承し_serialize/_deserializeを実装すれば、ドメイン固有の型変換と検証を1箇所に閉じ込めて再利用できる
- 不正入力は_deserialize内でValidationErrorを送出。default_error_messagesとmake_errorでメッセージをi18n可能にする
- 金額(Decimal丸め)・電話番号(E.164正規化)・列挙(fields.Enum)など、業務ルールを持つ値をカスタムフィールドとして設計する
- 算出値はfields.Method/Functionで、固定値はfields.Constantで宣言し、ビジネスロジックの散在を防ぐ
- 既存フィールドのサブクラス化とvalidate.Andの合成で、検証ロジックをDRYに組み立てる

---

## **導入：組み込み `fields` で足りなくなったとき**

`fields.Str()`、`fields.Int()`、`fields.Email()`——marshmallow の組み込みフィールドは強力で、API の入出力の大半はこれだけで賄えます。ところが実務のドメインには、組み込みでは表現しきれない**業務ルールを持った値**が必ず登場します。

- 金額：通貨は `Decimal` で持ち、小数第 2 位に丸め、`float` では絶対に扱わない。
- 電話番号：ユーザーは `090-1234-5678` とも `09012345678` とも入力するが、内部では常に E.164 形式 `+819012345678` に正規化したい。
- 郵便番号、SKU コード、社内 ID——「形式が決まっていて、検証と正規化が伴う」値。

こうした値を、ビュー関数やサービス層で「その都度 `if` 文で検証し、`.replace()` で整形する」のは典型的なアンチパターンです。同じ正規化ロジックが API ごとにコピーされ、ある場所では `+81` を付けるのに別の場所では付け忘れる——**知識が散在し、いつか必ず食い違います**。

ドメイン駆動設計でいう**値オブジェクト（Value Object）**の発想がここで効きます。「金額」や「電話番号」は単なる文字列ではなく、固有の制約を持つ型です。その制約を、システム境界で**一度だけ**適用し、内部には正規化済みの値だけを流し込む。marshmallow の**カスタムフィールド**は、まさにこの「境界での 1 度きりの正規化・検証」を、再利用可能な部品として表現するための仕組みです。

> 💡 本記事は marshmallow の基礎——`Schema` / `load()` / `dump()` / 三層バリデーション / セキュリティ境界——を解説した [marshmallow 実践ガイド（v4対応）](/blog/marshmallow-python-serialization-validation-production-guide) の続編です。`load()` が境界バリデーションの入口であること、`@validates` / `@validates_schema` の責務分離を前提に進めます。未読であればそちらを先にどうぞ。

> 💡 **この記事で扱うバージョン**：marshmallow **4.3.0**（2026年4月時点の安定版）を前提とします。v4 の重要な変更点として、`fields.Field` が**ジェネリック化**され、`class MyField(fields.Field[T])` と「このフィールドがデシリアライズ後に返す Python 型 `T`」を型引数で宣言できるようになりました。これは単なる飾りではなく、IDE 補完と静的解析の精度を上げる実利があります。

---

## **1. カスタムフィールドの解剖：`_serialize` と `_deserialize`**

カスタムフィールドの実装は、突き詰めれば**たった 2 つのメソッドを書く**ことに尽きます。`fields.Field[T]` を継承し、出力方向の `_serialize` と入力方向の `_deserialize` を実装します。

```python
from marshmallow import ValidationError, fields


class PinCode(fields.Field[list[int]]):
    """暗証番号を「内部では int のリスト、外向きには文字列」として双方向変換するフィールド。"""

    def _serialize(self, value, attr, obj, **kwargs):
        # dump 方向：内部の [1, 2, 3, 4] を "1234" という文字列にする
        if value is None:
            return ""
        return "".join(str(d) for d in value)

    def _deserialize(self, value, attr, data, **kwargs):
        # load 方向：外部の "1234" を内部の [1, 2, 3, 4] に変換し、不正なら弾く
        try:
            return [int(c) for c in value]
        except ValueError as error:
            raise ValidationError("Pin codes must contain only digits.") from error
```

2 つのメソッドの役割と引数は、公式仕様で次のように定まっています。

| メソッド | 方向 | 受け取る `value` | 返すべきもの |
| --- | --- | --- | --- |
| `_serialize(self, value, attr, obj, **kwargs)` | `dump`（出力） | 内部の Python 値 | JSON 互換の外向き表現 |
| `_deserialize(self, value, attr, data, **kwargs)` | `load`（入力） | 外部の生データ | 検証・正規化済みの内部値 `T` |

- `_serialize` の `obj` は**変換元のオブジェクト全体**、`_deserialize` の `data` は**入力 dict 全体**です。他フィールドの値を参照したいときに使えますが、まずは第 1 引数の `value` だけで完結させるのが基本です。
- `_deserialize` が返した値が、`load()` の結果 dict に入ります。`PinCode` なら `load({"pin": "1234"})` は `{"pin": [1, 2, 3, 4]}` を返し、`dump({"pin": [1, 2, 3, 4]})` は `{"pin": "1234"}` を返す——**往復が閉じている**のが要点です。

```python
from marshmallow import Schema


class LoginSchema(Schema):
    pin = PinCode()


LoginSchema().load({"pin": "1234"})   # → {'pin': [1, 2, 3, 4]}
LoginSchema().dump({"pin": [1, 2, 3, 4]})  # → {'pin': '1234'}
LoginSchema().load({"pin": "12ab"})   # → ValidationError: Pin codes must contain only digits.
```

**なぜこれが優れているのか？**
「文字列 `"1234"` を `int` のリストに変換し、数字以外なら拒否する」という知識が、`PinCode` という**ひとつの型**に凝集します。この型を使うすべてのスキーマが、自動的に同じ変換と検証を得る——典型的な DRY の実践です。さらに `fields.Field[list[int]]` という型引数のおかげで、「このフィールドの `_deserialize` は `list[int]` を返す」という契約が型として明示され、ETC（Easy To Change）にも効きます。変換ルールが変わっても、修正箇所は `PinCode` の中だけです。

---

## **2. エラー設計と i18n：`make_error` でメッセージを一元管理する**

`PinCode` ではエラーメッセージを `_deserialize` 内にベタ書きしました。動きはしますが、**メッセージが実装ロジックに埋め込まれている**ため、多言語対応や文言の統一が困難です。marshmallow は、この問題を `default_error_messages` と `make_error` で解決します。

```python
from marshmallow import ValidationError, fields


class PinCode(fields.Field[list[int]]):
    # フィールドクラスにエラーメッセージを「キー → 文言」の辞書として宣言する
    default_error_messages = {
        "invalid": "暗証番号は数字のみで構成してください。",
        "length": "暗証番号は4桁である必要があります。",
    }

    def _serialize(self, value, attr, obj, **kwargs):
        if value is None:
            return ""
        return "".join(str(d) for d in value)

    def _deserialize(self, value, attr, data, **kwargs):
        try:
            digits = [int(c) for c in value]
        except (TypeError, ValueError) as error:
            # メッセージ本文ではなく「キー」で参照する
            raise self.make_error("invalid") from error
        if len(digits) != 4:
            raise self.make_error("length")
        return digits
```

`self.make_error("invalid")` は、`default_error_messages` からキー `"invalid"` に対応する文言を引き、`ValidationError` を構築して返します。直接 `raise ValidationError("...")` と書くこともできますが、`make_error` には次の利点があります。

- **メッセージの一元管理**：すべての文言が `default_error_messages` 辞書に集約され、実装ロジックと分離される。
- **インスタンス単位の上書き**：呼び出し側がフィールド生成時に `error_messages={...}` を渡すと、同じキーの文言だけを差し替えられる。組み込みフィールドの `required` メッセージを和訳するのと同じ仕組みです。

```python
from marshmallow import Schema, fields


class CheckoutSchema(Schema):
    # 組み込みフィールドも同じ仕組み。キー単位でメッセージを上書きできる
    email = fields.Email(
        required=True,
        error_messages={"required": "メールアドレスは必須です。", "invalid": "メール形式が不正です。"},
    )
    # 自作フィールドの "length" だけ、この画面用に差し替える
    pin = PinCode(error_messages={"length": "4桁の暗証番号を入力してください。"})
```

**なぜこれが優れているのか？**
`make_error("invalid")` は、文言の**実体**ではなく**意味（キー）**を参照します。これにより、「数字以外が来た」という検証ロジックと、「ユーザーに何と伝えるか」という表示の関心が分離されます（SRP）。i18n リソースを差し込みたい、あるいは画面ごとに文言を変えたい——そうした要求が、`_deserialize` の中身を一切触らずに `error_messages` の差し替えだけで実現できる。これが「ロジックとメッセージを分けておく」ことの配当です。

> ⚠️ **`_deserialize` で例外を握り潰さない**：`[int(c) for c in value]` は `value` が `None` や `int` だと `TypeError` を、数字以外を含むと `ValueError` を投げます。これらを `try/except` で捕まえたら、**必ず `ValidationError`（または `make_error`）へ変換して再送出**してください。生の `TypeError` をそのまま外へ漏らすと、marshmallow のエラー集約機構（`err.messages` への構造化、`many=True` でのインデックス付け）をすり抜け、500 エラーとしてユーザーに露出します。`raise ... from error` で元例外を `__cause__` に保持しておくと、ログ調査の手がかりも失いません。

---

## **3. 実例①：金額フィールド（`Decimal` ベース）**

最も「自作する価値が高い」ドメイン型が金額です。金額を `float` で扱うのは、本番障害に直結する危険な選択です。

> ⚠️ **金額を `float` で扱ってはいけない**：`0.1 + 0.2` が `0.30000000000000004` になるのは有名ですが、これは IEEE 754 浮動小数点が 2 進数で 10 進小数を正確に表現できないために起こります。金額計算でこれが積み重なると、請求額が 1 円ずれる、合計が合わない、といった**会計上あってはならない誤差**になります。金額は必ず `decimal.Decimal` で保持し、JSON へは**文字列**として出力します（`float` で出力すると JSON パーサ側で再び誤差が混入するため）。

marshmallow には `fields.Decimal` があり、`places`（小数桁数）・`as_string`（文字列出力）・`rounding`（丸めモード）を備えています。これをサブクラス化して、**通貨ドメインの規約をデフォルトとして焼き込んだ** `MoneyField` を作ります。

```python
import decimal

from marshmallow import ValidationError, fields, validate


class MoneyField(fields.Decimal):
    """通貨額を表すフィールド。小数第2位・四捨五入（ROUND_HALF_UP）・文字列出力をデフォルトに固定する。"""

    default_error_messages = {
        "negative": "金額は0以上である必要があります。",
    }

    def __init__(self, *, allow_negative: bool = False, **kwargs):
        self.allow_negative = allow_negative
        # ドメイン規約をデフォルト値として設定する。呼び出し側が明示すれば上書き可能
        kwargs.setdefault("places", 2)
        kwargs.setdefault("rounding", decimal.ROUND_HALF_UP)
        kwargs.setdefault("as_string", True)  # JSON には文字列で出す（精度保持）
        super().__init__(**kwargs)

    def _deserialize(self, value, attr, data, **kwargs):
        # まず親（fields.Decimal）に Decimal への変換・桁丸めを任せる
        amount = super()._deserialize(value, attr, data, **kwargs)
        if not self.allow_negative and amount < 0:
            raise self.make_error("negative")
        return amount
```

使い方は組み込みフィールドと変わりません。

```python
from marshmallow import Schema


class OrderSchema(Schema):
    subtotal = MoneyField(required=True)
    # 値引きはここでは正負の検証だけ。Range など追加ルールは validate= で合成できる
    discount = MoneyField(allow_negative=False, validate=validate.Range(min=0))


OrderSchema().load({"subtotal": "1980.005", "discount": "200"})
# → {'subtotal': Decimal('1980.01'), 'discount': Decimal('200.00')}  ← 桁丸め済み
OrderSchema().dump({"subtotal": decimal.Decimal("1980.01")})
# → {'subtotal': '1980.01', ...}  ← float ではなく文字列で出力
```

ポイントは、`_deserialize` で**まず親に変換を委譲**してから（`super()._deserialize(...)`）、その結果に対してドメイン固有の検証（負数チェック）を追加していることです。`Decimal` への変換と桁丸めという「車輪」を再発明せず、業務ルールだけを上乗せしています。

**なぜこれが優れているのか？**
「金額は小数第 2 位・四捨五入・文字列出力・負数禁止」という通貨ドメインの規約が、`MoneyField` という**ひとつの値オブジェクト**に封じ込められます。プロジェクト中のすべての金額フィールドを `MoneyField` で宣言すれば、ある API だけ `float` で出してしまう、ある画面だけ丸めを忘れる、といった**規約のほころびが構造的に起こり得なくなります**。筆者が木材流通の B2B SaaS で請求・決済まわりを設計した際も、金額の取り扱いを型レベルで統一することが、課金の正確性を担保する土台でした。

---

## **4. 実例②：電話番号フィールド（E.164 正規化）**

電話番号は「ユーザーの入力形式が多様」かつ「内部では 1 つの正規形に統一したい」典型例です。`090-1234-5678`、`09012345678`、`+81 90 1234 5678`——これらをすべて E.164 形式 `+819012345678` に正規化し、不正な入力は弾くフィールドを作ります。

```python
import re

from marshmallow import ValidationError, fields

# 国内番号（0始まり、10〜11桁）を想定したシンプルな正規化例
_DOMESTIC_RE = re.compile(r"^0\d{9,10}$")


class PhoneField(fields.Field[str]):
    """日本国内の電話番号を E.164 形式（+81...）に正規化するフィールド。"""

    default_error_messages = {
        "invalid": "電話番号の形式が正しくありません。",
    }

    def _serialize(self, value, attr, obj, **kwargs):
        # 内部では常に +81... の正規形なので、そのまま出力する
        return value if value is not None else None

    def _deserialize(self, value, attr, data, **kwargs):
        if not isinstance(value, str):
            raise self.make_error("invalid")
        # 数字以外（ハイフン・空白・括弧）を除去してから判定する
        digits = re.sub(r"[^\d]", "", value)
        if _DOMESTIC_RE.match(digits):
            # 先頭の 0 を国番号 +81 に置き換える（E.164 化）
            return "+81" + digits[1:]
        # 既に +81 始まりの国際形式で来たケースも受理する
        if value.startswith("+81") and re.fullmatch(r"\+81\d{9,10}", value):
            return value
        raise self.make_error("invalid")
```

```python
from marshmallow import Schema


class ContactSchema(Schema):
    phone = PhoneField(required=True)


ContactSchema().load({"phone": "090-1234-5678"})  # → {'phone': '+819012345678'}
ContactSchema().load({"phone": "09012345678"})    # → {'phone': '+819012345678'}
ContactSchema().load({"phone": "+819012345678"})  # → {'phone': '+819012345678'}
ContactSchema().load({"phone": "1234"})           # → ValidationError: 電話番号の形式が正しくありません。
```

> 💡 上記は正規化の**設計パターン**を示すための最小実装です。実運用で国際番号・固定電話・桁数の地域差まで厳密に扱うなら、`phonenumbers`（Google libphonenumber の Python 移植）を `_deserialize` 内で呼び出し、`phonenumbers.format_number(..., E164)` の結果を返す構成にすると堅牢です。要は「正規化と検証のロジックを `_deserialize` の中に閉じ込める」という構造が本質で、中身のアルゴリズムは差し替え可能です。

**なぜこれが優れているのか？**
入力形式の揺れ（ハイフンあり／なし、国際表記）を吸収して**唯一の正規形**に揃える責務が、`PhoneField` ひとつに集約されます。これを全 API の電話番号入力に使えば、DB に保存される電話番号は**必ず E.164**という不変条件が境界で保証されます。下流の SMS 送信・重複チェック・名寄せといった処理は、「電話番号は正規化済み」という前提を信頼でき、各処理が個別に正規化を再実装する必要がなくなります。これは「境界で 1 度だけ正規化する」という値オブジェクトの思想そのものです。

---

## **5. 実例③：列挙（`fields.Enum`）で Python の `Enum` を境界で安全に扱う**

ステータスや種別といった「決められた選択肢のいずれか」は、Python 標準の `enum.Enum` で表現するのが型安全です。marshmallow には専用の `fields.Enum` があり、これを使うと**外部からの文字列を境界で安全に `Enum` メンバーへ変換**できます。`fields.Enum` は自作不要——組み込みで十分です。

```python
import enum

from marshmallow import Schema, fields


class OrderStatus(enum.Enum):
    PENDING = "pending"
    PAID = "paid"
    SHIPPED = "shipped"
    CANCELLED = "cancelled"


class OrderSchema(Schema):
    # デフォルト：メンバーの「名前」でシリアライズ／デシリアライズする
    status_by_name = fields.Enum(OrderStatus)
    # by_value=True：メンバーの「値」でシリアライズ／デシリアライズする
    status_by_value = fields.Enum(OrderStatus, by_value=True)
```

2 つのモードの違いは、外向きの表現に**名前を使うか、値を使うか**です。

| 宣言 | `load` が受け付ける入力 | `dump` の出力 |
| --- | --- | --- |
| `fields.Enum(OrderStatus)` | `"PAID"`（メンバー名） | `"PAID"` |
| `fields.Enum(OrderStatus, by_value=True)` | `"paid"`（メンバー値） | `"paid"` |

```python
schema = OrderSchema()
schema.load({"status_by_name": "PAID", "status_by_value": "paid"})
# → {'status_by_name': <OrderStatus.PAID: 'paid'>, 'status_by_value': <OrderStatus.PAID: 'paid'>}

# 列挙にない値は ValidationError で弾かれる
schema.load({"status_by_name": "UNKNOWN", "status_by_value": "paid"})
# → ValidationError: Must be one of: PENDING, PAID, SHIPPED, CANCELLED.
```

どちらを選ぶかは、**外部 API の契約**で決まります。フロントエンドや外部システムが `"paid"`（小文字の値）でやり取りするなら `by_value=True`、`"PAID"`（メンバー名）でやり取りするなら既定のままです。

**なぜこれが優れているのか？**
列挙の選択肢チェックを `validate.OneOf(["pending", "paid", ...])` で文字列リストとして書くと、リストと実際の `Enum` 定義が**二重管理**になり、片方を変更し忘れる DRY 違反を招きます。`fields.Enum(OrderStatus)` は `Enum` 定義そのものを唯一の真実とし、`load()` を通過した時点で値は生の文字列ではなく**型付きの `Enum` メンバー**になります。下流のコードは `if order.status is OrderStatus.PAID:` と `is` 比較で安全に分岐でき、文字列のタイポによる分岐漏れが型レベルで防がれます。

---

## **6. 算出・派生値：`fields.Method` / `Function` / `Constant`**

すべてのフィールドが入力に対応するわけではありません。「他のフィールドから計算される値」「サーバーが固定で返す値」を**出力専用**で宣言したい場面があります。marshmallow は 3 つの軽量な仕組みを用意しています。

### **`fields.Function`：その場のラムダで算出する**

```python
from marshmallow import Schema, fields


class UserSchema(Schema):
    first_name = fields.Str()
    last_name = fields.Str()
    # obj（変換元オブジェクト全体）を受け取り、その場で算出する
    upper_name = fields.Function(lambda obj: obj["first_name"].upper())
```

`fields.Function` は `serialize` 用の呼び出し可能オブジェクトを取り、必要なら `deserialize=` で逆方向も指定できます。**1 行で完結する単純な算出**に向きます。

### **`fields.Method`：スキーマのメソッドに委ねる**

算出ロジックが複雑、あるいは複数フィールドで共有したいなら、スキーマのメソッドを指す `fields.Method` が読みやすくなります。

```python
from marshmallow import Schema, fields


class OrderSchema(Schema):
    subtotal = MoneyField(required=True)
    tax_rate = fields.Decimal(load_default="0.10")
    # serialize 時は total_amount を、deserialize 時は parse_total を呼ぶ
    total = fields.Method("total_amount", deserialize="parse_total")

    def total_amount(self, obj):
        # 小計 × (1 + 税率) を算出して返す
        return str(obj["subtotal"] * (1 + obj["tax_rate"]))

    def parse_total(self, value):
        # 入力側で受け取りたい場合の変換（不要なら deserialize= を省略すれば dump 専用になる）
        return decimal.Decimal(value)
```

`fields.Method` のシリアライズメソッドは `def method(self, obj): ...`、デシリアライズメソッドは `def method(self, value): ...` という署名を取ります。

### **`fields.Constant`：常に同じ値を出力する**

API のバージョンタグやレスポンスの種別など、**入力に依らず常に固定の値**を返すには `fields.Constant` です。

```python
from marshmallow import Schema, fields


class ResponseSchema(Schema):
    api_version = fields.Constant("2026-06")  # 常にこの値を出力する
    object_type = fields.Constant("order")
```

### **`dump_only` との組み合わせ**

算出・派生値は本質的に**出力専用**であることがほとんどです。クライアントに `total` を書き込ませてしまうと改ざんの余地になるため、`dump_only=True` を併用して入力を遮断します。

```python
class OrderSchema(Schema):
    subtotal = MoneyField(required=True)
    # 算出値はサーバーが決める。load では受け付けない
    total = fields.Function(lambda obj: str(obj["subtotal"] * decimal.Decimal("1.10")), dump_only=True)
```

**なぜこれが優れているのか？**
「合計金額の算出」「バージョンタグの付与」といったロジックを、`fields.Method` / `Function` / `Constant` として**スキーマ定義に宣言**すると、レスポンス整形の知識がスキーマに凝集します。ビュー関数の中で `response["total"] = subtotal * 1.1` のように手で組み立てる必要がなくなり、ビジネスロジックがプレゼンテーション層に漏れ出すのを防げます（SRP）。出力仕様が変わっても、修正はスキーマの中だけで完結します。

---

## **7. 既存フィールドの拡張：サブクラス化と `_bind_to_schema`**

ゼロから `fields.Field[T]` を継承するだけが手ではありません。**既存フィールドをサブクラス化**し、`_deserialize` をオーバーライドして共通の前処理を常時かけるのは、極めて実用的なパターンです。

代表例が「文字列を常にトリム（前後空白除去）する」フィールドです。`@pre_load` でスキーマごとに書くより、**型として持たせる**ほうが再利用が効きます。

```python
from marshmallow import fields


class TrimmedString(fields.String):
    """前後の空白を常に除去する文字列フィールド。空白起因のバグを型レベルで防ぐ。"""

    def __init__(self, *, lower: bool = False, **kwargs):
        self.lower = lower
        super().__init__(**kwargs)

    def _deserialize(self, value, attr, data, **kwargs):
        # まず親（fields.String）に文字列としての検証を任せる
        result = super()._deserialize(value, attr, data, **kwargs)
        result = result.strip()
        if self.lower:
            result = result.lower()
        return result
```

```python
from marshmallow import Schema, validate


class SignupSchema(Schema):
    name = TrimmedString(required=True, validate=validate.Length(min=1))
    # メールは小文字化＋トリムを型に焼き込む。validate= の検証はトリム後の値にかかる
    email = TrimmedString(lower=True, validate=validate.Email())


SignupSchema().load({"name": "  友田  ", "email": " Tomoda@Example.com "})
# → {'name': '友田', 'email': 'tomoda@example.com'}
```

`super()._deserialize(...)` で親の「文字列型としての検証」を再利用しつつ、`strip()` / `lower()` という正規化だけを上乗せしている点に注目してください。**車輪の再発明をせず、振る舞いだけを足す**——これが拡張の王道です。

### **`_bind_to_schema`：スキーマ束縛時のフック**

フィールドが「自分が属するスキーマ」を知る必要があるとき——たとえば親スキーマの設定に応じてエラーメッセージを切り替える、別フィールドへの参照を解決する、といった高度なケース——では、`_bind_to_schema` フックに触れます。これはフィールドがスキーマに紐付けられる瞬間に呼ばれます。

```python
from marshmallow import fields


class ContextAwareField(fields.String):
    def _bind_to_schema(self, field_name, parent):
        # v4 ではフックの第2引数が `schema` から `parent` に改名された点に注意
        super()._bind_to_schema(field_name, parent)
        # ここで self.parent（属するスキーマ）や field_name にアクセスできる
        # 例：親スキーマの状態に応じてこのフィールドの挙動を初期化する
```

> 💡 `_bind_to_schema` は marshmallow 4 で**引数名が `schema` から `parent` に変更**されました（フィールドはスキーマだけでなく別フィールドの内側にも束縛され得るため、より正確な名前になりました）。3.x のコードを移植する際は署名を合わせてください。日常的に触る機会は多くありませんが、フィールドにスキーマ文脈を持たせたい高度な拡張で役立ちます。

**なぜこれが優れているのか？**
`TrimmedString` は、「入力文字列は常にトリムされている」という不変条件を**型に焼き込み**ます。`@pre_load` でスキーマごとに正規化を書く方式と比べ、フィールドを宣言した時点で正規化が保証されるため、書き忘れが起こり得ません。コンポジション（既存フィールド＋追加の振る舞い）でドメイン型を組み立てるこのやり方は、CLAUDE.md でいう「継承より合成」「ETC」の体現です。

---

## **8. バリデータの合成：`validate.And` と再利用可能な関数**

カスタムフィールドが「型変換と正規化」を担うのに対し、`validate=` 引数は「値が満たすべき制約」を宣言します。両者は補完関係にあり、**1 つのフィールドに複数のバリデータを合成**できます。

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


class ProductSchema(Schema):
    # リストで渡すと、すべてのバリデータが順に適用される
    sku = fields.Str(validate=[validate.Length(equal=8), validate.Regexp(r"^[A-Z0-9]+$")])
    # validate.And は同じ合成を1つのバリデータオブジェクトとして表現する（再利用しやすい）
    code = fields.Str(validate=validate.And(validate.Length(equal=8), validate.Regexp(r"^[A-Z0-9]+$")))
```

`validate=[a, b]`（リスト）と `validate.And(a, b)`（合成オブジェクト）は同じ効果ですが、`validate.And(...)` は**合成済みバリデータを変数に束ねて使い回せる**点で優れます。プロジェクト共通の制約は、名前を付けて 1 箇所で定義しましょう。

```python
from marshmallow import validate

# プロジェクト共通の SKU 制約を「ひとつの再利用可能なバリデータ」として定義する
sku_validator = validate.And(
    validate.Length(equal=8),
    validate.Regexp(r"^[A-Z0-9]+$", error="SKUは英大文字と数字のみ8桁です。"),
)


class ProductSchema(Schema):
    sku = fields.Str(validate=sku_validator)  # 使い回す


class InventorySchema(Schema):
    sku = fields.Str(validate=sku_validator)  # 別スキーマでも同じ制約
```

### **自作バリデータ関数：`ValidationError` を `raise` する**

組み込みバリデータで表現できない制約は、**`ValidationError` を送出する関数**として書きます。

```python
from marshmallow import ValidationError


def validate_even(value: int) -> None:
    # v4 ではバリデータは False を返すのではなく ValidationError を raise する
    if value % 2 != 0:
        raise ValidationError("偶数を指定してください。")


class LotSchema(Schema):
    quantity = fields.Int(validate=[validate.Range(min=0), validate_even])
```

> ⚠️ **v4 のバリデータは `False` を返してはいけない**：marshmallow 3.x では `validate=lambda x: x > 0` のように `bool` を返すバリデータが使えましたが、**v4 では検証失敗時に必ず `ValidationError` を `raise`** する必要があります。`return False` は無視され、検証がすり抜けます。生成 AI が出力するコードや古い記事は 3.x スタイルのことが多いので、移植時は要注意です。

### **`validate=` と `@validates` の使い分け**

| 手段 | 適する場面 | 値以外の文脈 |
| --- | --- | --- |
| `validate=`（バリデータ／合成） | 値**単体**で完結する静的ルール（長さ・範囲・正規表現）。再利用が容易 | 使えない（値だけを見る） |
| `@validates("field")` | スキーマのインスタンスや他の状態を参照する必要があるフィールド検証 | スキーマの `self` にアクセスできる |
| `@validates_schema` | **複数フィールドにまたがる**不変条件（開始 < 終了 など） | 入力 dict 全体を参照できる |

原則は「**値単体の静的ルールは `validate=` で再利用可能に、文脈が要るなら `@validates` 系で**」です。`validate=` に寄せられるものを `@validates` に書くと、再利用が効かず、スキーマごとに同じロジックが重複します。

**なぜこれが優れているのか？**
`sku_validator` のように制約に名前を付けて合成しておくと、その制約が**ひとつの再利用可能な部品**になります。SKU の桁数仕様が変わったとき、修正は `sku_validator` の定義 1 箇所で済み、それを使う全スキーマに自動的に反映されます。「検証ルールの単一の真実」を保つ——これは DRY を検証の領域にまで徹底する設計です。

---

## **結論：ドメイン型を「境界の部品」として設計する**

組み込み `fields` で足りなくなったとき、その不足は「ドメイン固有の値オブジェクトを設計せよ」というサインです。marshmallow のカスタムフィールドは、その値オブジェクトを境界で再利用可能な部品として表現する、実績ある仕組みです。本記事の要点を再掲します。

1. **`fields.Field[T]` を継承し `_serialize` / `_deserialize` を実装**すれば、ドメイン固有の型変換と検証を 1 箇所に閉じ込め、すべてのスキーマで再利用できる。
2. 不正入力は `_deserialize` 内で **`ValidationError` へ変換して送出**し、生の例外を漏らさない。**`default_error_messages` + `make_error`** で文言を一元管理し、`error_messages` で i18n・画面別の差し替えに開いておく。
3. **金額は `Decimal`・文字列出力・丸めを焼き込んだ `MoneyField`**、**電話番号は E.164 に正規化する `PhoneField`** として、業務ルールを持つ値を型にする。`float` で金額を扱わない。
4. **列挙は `fields.Enum`**（`by_value=True` で値ベース）で、生文字列ではなく型付きメンバーとして境界を通す。
5. **算出値は `fields.Method` / `Function`、固定値は `fields.Constant`** で宣言し、`dump_only` と併せてビジネスロジックの散在を防ぐ。
6. **既存フィールドのサブクラス化**（`TrimmedString`）と **`validate.And` による合成**で、検証ロジックを DRY に組み立てる。バリデータは v4 では必ず `ValidationError` を `raise` する。

「動くコード」と「10 年運用できるコード」の差は、**ドメインの値をどこで・どう正規化・検証し、その知識をどれだけ局所化できるか**にあります。カスタムフィールドは、境界での 1 度きりの正規化を再利用可能な型へと昇華させ、規約のほころびを構造的に不可能にします。

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

- [Custom Fields](https://marshmallow.readthedocs.io/en/stable/custom_fields.html)
- [API Reference: fields](https://marshmallow.readthedocs.io/en/stable/marshmallow.fields.html)
- [API Reference: validate](https://marshmallow.readthedocs.io/en/stable/marshmallow.validate.html)
- [Upgrading to newer releases（3→4 移行）](https://marshmallow.readthedocs.io/en/stable/upgrading.html)

---

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

筆者は、ここで解説した「ドメインの値を境界で 1 度だけ正規化・検証し、内部には型付きの安全な値だけを流す」という規律を、経済産業大臣賞を受賞した B2B SaaS の本番環境で実装・運用してきました。金額・電話番号・各種コードといった業務ルールを持つ値を、再利用可能なカスタムフィールドとして設計することは、課金の正確性・データの一貫性・保守性に直結します。型安全な入力検証・ドメイン型の設計・レスポンス整形・ORM 連携といった、**事業の信頼性に直結する基盤**を、生成 AI を活用して高速かつ高品質に構築します。Python を用いたバックエンド開発・既存システムの型安全化について、お気軽にご相談ください。
