メインコンテンツへスキップ
友田 陽大
marshmallow
Python
marshmallow
シリアライズ
バリデーション
型安全
Flask
SQLAlchemy
アーキテクチャ設計

marshmallow 実践ガイド:Python オブジェクトのシリアライズ/検証を境界で堅牢に設計する(v4対応)

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

公開日
読了時間
20分
著者
友田 陽大
シェア
目次

導入:なぜ今あえて marshmallow なのか

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

**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)に忠実でありながら、それより一段わかりやすく整理したものです。

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

💡 この記事は Python バックエンド設計の連作の一部です。型ファーストの対になる選択肢は Pydantic v2 実践ガイド、Web フレームワーク層は FastAPI 本番運用ガイド、永続化層は SQLAlchemy 2.0 実践ガイド を併せて読むと、境界から DB までの一貫した設計が見渡せます。


1. Schemafields:双方向の変換を宣言的に定義する

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

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()実行時にスキーマを生成することもできます。設定値から動的にスキーマを組み立てたいときに有効です。

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

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

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

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 の最重要機能です。

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 のときは、エラーが要素のインデックスをキーとして返るため、「配列の何番目の、どのフィールドが、なぜ落ちたか」が一目で分かります。

# {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(出力時のデフォルト)に統一されました。「入力と出力で意味が違う」ことが名前から明確になる、優れた変更です。

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 がこの翻訳を境界に閉じ込めます。

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 をスキップできます。

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() では完全に無視されます。

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() の出力には決して含まれません。

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) のような無防備な展開とは対極の、安全側に倒れた設計です。挙動は明示的に切り替えられます。

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 のバリデータを渡す方法です。

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 ではバリデータは必ず ValidationErrorraise しなければなりませんFalse を返す書き方は動かなくなります。

第二層:@validates(フィールド単位のカスタム検証)

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

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 に集約します。

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 を渡します。

    @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:検証前に入力を正規化する

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

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 ではなく型付きオブジェクト」という設計を実現します。

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 から改名)を指定します。

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.Fieldpre_load / post_load 引数が追加され、フィールド単位の前後処理をデコレータなしで宣言できるようになりました。スキーマ全体ではなく特定フィールドだけを整形したいケースで有用です(詳細は公式チェンジログを参照)。


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

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

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 で絞り込めます。ドット記法で多階層も指定できます。

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 が最短です。

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

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

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

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 モデルからスキーマを自動生成でき、ボイラープレートが激減します。

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 で返します。

@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 34 移行:本番で踏む変更点の早見表

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

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 を返すValidationErrorraise するバリデータの返り値
@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 / defaultload_default / dump_defaultpass_manypass_collection です。grep -rn "missing=\|default=\|pass_many=\|\.context" . で機械的に洗い出すのが確実です。fields.Number() の直接利用は、意図した型(整数か小数か)を明示する好機でもあります。


9. marshmallowPydantic か:選定の軸

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

観点marshmallowPydantic 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 実践ガイド で詳説しています。


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

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 は、その境界を宣言的なスキーマとして表現する、実績ある道具です。

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


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

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

友田

友田 陽大

経済産業大臣賞 受賞プロダクト開発者。TypeScript + Python + AWS で、SaaS・業界DX・ 実用レベルの生成AI(RAG)を、要件定義からインフラ・運用まで一人で完遂します。

この記事で解説した技術の適用事例

経済産業大臣賞受賞 | 木材流通業界のDXを実現したB2BサブスクリプションSaaS

ケーススタディを見る