Pythonライブラリ Marshmallow 徹底解説:データバリデーションとシリアライズ/デシリアライズの決定版 ✨

Pythonでのデータ処理を劇的に効率化するMarshmallowの全てを学びましょう!

Marshmallowとは? 🤔

Marshmallowは、Pythonで複雑なデータ型(オブジェクトなど)とネイティブなPythonデータ型(辞書、リストなど)の間で相互変換を行うための、ORM/ODM/フレームワーク非依存のライブラリです。 主に以下の3つの強力な機能を提供します。

  • 入力データのバリデーション(検証): APIリクエストやフォーム入力など、外部から受け取ったデータが正しい形式・値を持っているかを確認します。
  • デシリアライズ: JSON文字列やPython辞書などのデータを、アプリケーションで扱いやすいオブジェクトに変換します。
  • シリアライズ: アプリケーション内のオブジェクトを、JSONなどの標準的な形式で出力するために、プリミティブなPythonデータ型(辞書など)に変換します。

これにより、API開発、設定ファイルの読み書き、データ変換処理など、様々な場面でコードの記述量を削減し、堅牢性と可読性を向上させることができます。特にWebフレームワーク(FlaskやDjangoなど)との親和性が高く、APIのエンドポイントでリクエストデータを受け取り、レスポンスデータを返す際の処理を非常に簡潔に記述できます。👍

なぜMarshmallowなのか?
Pythonには標準のjsonライブラリやpickleがありますが、これらは単純なデータ変換には使えても、複雑なバリデーションルールやネストされた構造の扱いは得意ではありません。Marshmallowは、スキーマを定義することで、データの構造、型、バリデーションルールを一元管理でき、より宣言的で再利用可能なコードを実現します。

インストール 🚀

Marshmallowはpipを使って簡単にインストールできます。

pip install -U marshmallow

最新の機能を利用するために、-Uオプションで最新版にアップグレードすることをお勧めします。

基本的な使い方 基礎編 🧱

1. スキーマの定義

Marshmallowの中心となる概念はスキーマ (Schema) です。スキーマは、データの構造、各フィールドの型、そしてバリデーションルールを定義します。marshmallow.Schema クラスを継承し、フィールドをクラス変数として定義します。

例として、ユーザー情報を表すスキーマを定義してみましょう。

from marshmallow import Schema, fields, validate

class UserSchema(Schema):
    id = fields.Int(dump_only=True)  # 読み取り専用(シリアライズ時のみ出力)
    username = fields.Str(required=True, validate=validate.Length(min=3)) # 必須、最小3文字
    email = fields.Email(required=True) # 必須、Email形式
    age = fields.Int(validate=validate.Range(min=0)) # 0以上の整数
    created_at = fields.DateTime(dump_only=True) # 読み取り専用
    is_active = fields.Bool(load_default=True) # デシリアライズ時のデフォルト値

上記の例では、UserSchema というスキーマを定義しました。

  • fields.Int, fields.Str, fields.Email, fields.DateTime, fields.Bool など、様々なデータ型に対応するフィールドクラスが用意されています。
  • required=True で、そのフィールドが必須であることを示します。
  • dump_only=True はシリアライズ(オブジェクトから辞書へ)時にのみフィールドを含め、デシリアライズ(辞書からオブジェクトへ)時には無視することを示します。APIのレスポンスでIDや作成日時を含めたいが、リクエストボディでは受け付けたくない場合に便利です。
  • load_default=True はデシリアライズ時に値が指定されなかった場合のデフォルト値を設定します。
  • validate 引数には、marshmallow.validate モジュールにある組み込みバリデータ(Length, Range, OneOf など)や、カスタムバリデーション関数/メソッドを指定できます。

主要なフィールド型

Marshmallowには多くのフィールド型が用意されています。いくつか代表的なものを紹介します。

フィールドクラス 説明 Python型 (シリアライズ/デシリアライズ)
fields.Str 文字列 str
fields.Int 整数 int
fields.Float 浮動小数点数 float
fields.Decimal 固定小数点数 Decimal (デシリアライズ時), str (シリアライズ時デフォルト)
fields.Bool 真偽値 bool
fields.DateTime 日時 (aware/naive) datetime (デシリアライズ時), ISO 8601形式文字列 (シリアライズ時デフォルト)
fields.Date 日付 date (デシリアライズ時), ISO 8601形式文字列 (シリアライズ時デフォルト)
fields.Time 時間 time (デシリアライズ時), ISO 8601形式文字列 (シリアライズ時デフォルト)
fields.TimeDelta 時間差 timedelta (デシリアライズ時), 秒数を表す数値 (シリアライズ時デフォルト)
fields.UUID UUID UUID (デシリアライズ時), 文字列 (シリアライズ時デフォルト)
fields.URL URL文字列(検証付き) str
fields.Email Email文字列(検証付き) str
fields.List リスト list
fields.Tuple タプル tuple
fields.Dict 辞書 dict
fields.Nested ネストしたスキーマ スキーマによる
fields.Method メソッドの戻り値をフィールドとする メソッドの戻り値による
fields.Function 関数の戻り値をフィールドとする 関数の戻り値による

これらは一部であり、さらに多くのフィールド型やカスタマイズオプションが存在します。

2. シリアライズ (オブジェクト → データ)

アプリケーション内のPythonオブジェクト(クラスインスタンスや辞書など)を、JSONやAPIレスポンスに適した形式(通常はPythonの辞書)に変換するプロセスをシリアライズ (Serialization) と呼びます。Marshmallowではスキーマの dump() メソッドを使います。

import datetime

class User:
    def __init__(self, id, username, email, age, created_at=None, is_active=True):
        self.id = id
        self.username = username
        self.email = email
        self.age = age
        self.created_at = created_at or datetime.datetime.now()
        self.is_active = is_active

    def __repr__(self):
        return f"<User(username={self.username})>"

# Userオブジェクトを作成
user_obj = User(id=1, username="testuser", email="test@example.com", age=30)

# スキーマをインスタンス化
schema = UserSchema()

# dump()メソッドでシリアライズ
result = schema.dump(user_obj)
print(result)
# 出力例:
# {'is_active': True, 'email': 'test@example.com', 'id': 1, 'username': 'testuser', 'created_at': '2025-04-02T03:49:00.123456+00:00', 'age': 30}

# JSON文字列に変換したい場合は dumps() を使う
json_output = schema.dumps(user_obj)
print(json_output)
# 出力例:
# {"is_active": true, "email": "test@example.com", "id": 1, "username": "testuser", "created_at": "2025-04-02T03:49:00.123456+00:00", "age": 30}

dump() はPythonの辞書を返します。dumps() はJSON形式の文字列を返します。dump_only=True が指定された idcreated_at が出力に含まれていることがわかります。

複数のオブジェクトを一度にシリアライズしたい場合は、スキーマをインスタンス化する際に many=True を指定します。

user1 = User(id=1, username="user1", email="user1@example.com", age=25)
user2 = User(id=2, username="user2", email="user2@example.com", age=35)
users = [user1, user2]

# many=Trueを指定
many_schema = UserSchema(many=True)
result_many = many_schema.dump(users)
print(result_many)
# 出力例:
# [
#   {'is_active': True, 'email': 'user1@example.com', 'id': 1, 'username': 'user1', 'created_at': '...', 'age': 25},
#   {'is_active': True, 'email': 'user2@example.com', 'id': 2, 'username': 'user2', 'created_at': '...', 'age': 35}
# ]

出力フィールドのフィルタリング (`only`, `exclude`)

特定のフィールドだけを出力したり、特定のフィールドを除外したりすることも可能です。スキーマのインスタンス化時、または dump() メソッド呼び出し時に only または exclude 引数を指定します。

# usernameとemailのみ出力
summary_schema = UserSchema(only=("username", "email"))
summary_result = summary_schema.dump(user_obj)
print(summary_result)
# {'username': 'testuser', 'email': 'test@example.com'}

# ageとis_activeを除外して出力
detail_schema = UserSchema(exclude=("age", "is_active"))
detail_result = detail_schema.dump(user_obj)
print(detail_result)
# {'email': 'test@example.com', 'id': 1, 'username': 'testuser', 'created_at': '...'}

# dump時に指定も可能
result_only = schema.dump(user_obj, only=("id", "username"))
print(result_only)
# {'id': 1, 'username': 'testuser'}

3. デシリアライズ (データ → オブジェクト)

JSONデータやPython辞書など、外部から受け取ったデータを、アプリケーションで利用可能なPythonオブジェクトに変換するプロセスをデシリアライズ (Deserialization) と呼びます。Marshmallowではスキーマの load() メソッドを使います。デシリアライズ時には、スキーマで定義されたバリデーションも同時に実行されます。

from pprint import pprint

input_data = {
    "username": "newuser",
    "email": "new@example.com",
    "age": "28", # 文字列でもIntフィールドは整数に変換しようとする
    # is_active は省略(load_default=True が適用される)
}

schema = UserSchema()

try:
    # load()メソッドでデシリアライズとバリデーションを実行
    loaded_data = schema.load(input_data)
    pprint(loaded_data)
    # 出力例:
    # {'username': 'newuser', 'email': 'new@example.com', 'age': 28, 'is_active': True}
except ValidationError as err:
    print("バリデーションエラー発生!")
    pprint(err.messages)

load() は、バリデーションを通過した場合、変換された値を含む辞書を返します。age が文字列 “28” でしたが、fields.Int によって整数 28 に変換されています。is_active は入力に含まれていませんでしたが、load_default=True により True が設定されました。dump_only=True が指定されたフィールド(id, created_at)は無視されます。

JSON文字列から直接デシリアライズする場合は loads() を使います。

json_input = '{"username": "fromjson", "email": "json@example.com", "age": 40}'

try:
    loaded_from_json = schema.loads(json_input)
    pprint(loaded_from_json)
    # 出力例:
    # {'username': 'fromjson', 'email': 'json@example.com', 'age': 40, 'is_active': True}
except ValidationError as err:
    print("バリデーションエラー発生!")
    pprint(err.messages)

デシリアライズしてオブジェクトを生成する (`@post_load`)

load() のデフォルトの戻り値は辞書ですが、多くの場合、デシリアライズ結果を特定のクラスのインスタンスとして受け取りたいでしょう。これには @post_load デコレータを使います。

from marshmallow import Schema, fields, post_load, ValidationError

class UserSchemaWithPostLoad(Schema):
    # フィールド定義は UserSchema と同じ ...
    id = fields.Int(dump_only=True)
    username = fields.Str(required=True, validate=validate.Length(min=3))
    email = fields.Email(required=True)
    age = fields.Int(validate=validate.Range(min=0))
    created_at = fields.DateTime(dump_only=True)
    is_active = fields.Bool(load_default=True)

    @post_load
    def make_user(self, data, **kwargs):
        # バリデーション済みの辞書 data を受け取り、User オブジェクトを生成して返す
        # **kwargs は将来の拡張性のために受け取るのが一般的
        return User(**data) # ここで Userクラスのインスタンスを生成

# スキーマをインスタンス化
schema_with_post_load = UserSchemaWithPostLoad()

input_data = {
    "username": "objectuser",
    "email": "object@example.com",
    "age": 35
}

try:
    user_instance = schema_with_post_load.load(input_data)
    print(user_instance)
    # 出力: 
    print(f"Email: {user_instance.email}, Age: {user_instance.age}, Active: {user_instance.is_active}")
    # 出力: Email: object@example.com, Age: 35, Active: True
except ValidationError as err:
    print("バリデーションエラー発生!")
    pprint(err.messages)

@post_load で修飾されたメソッド (例: make_user) は、バリデーションと基本的な型変換が終わった後のデータ辞書を受け取ります。このメソッド内で、受け取ったデータを使って任意のオブジェクト(この場合は User クラスのインスタンス)を生成して返すことができます。これにより、load() メソッドは辞書の代わりに指定したオブジェクトを返すようになります。これは、APIリクエストから直接モデルオブジェクトを作成する場合などに非常に便利です。

4. バリデーション (検証)

Marshmallowの最も重要な機能の一つがデータバリデーションです。load()loads() を呼び出すと、スキーマに定義されたルールに基づいて入力データが自動的に検証されます。

バリデーションルールは、フィールド定義時に validate 引数を使って指定します。marshmallow.validate モジュールには、よく使われるバリデータが多数用意されています。

  • validate.Length(min=None, max=None, equal=None): 文字列やリストの長さを検証
  • validate.Range(min=None, max=None): 数値が指定範囲内にあるか検証
  • validate.OneOf(choices, labels=None): 値が許可された選択肢のいずれかであるか検証
  • validate.NoneOf(iterable, labels=None): 値が禁止された値のいずれでもないか検証
  • validate.Regexp(regex, flags=0): 正規表現にマッチするか検証
  • validate.Email(): 有効なEmail形式か検証 (fields.Email には自動で適用)
  • validate.URL(relative=False, require_tld=True): 有効なURL形式か検証 (fields.URL には自動で適用)

バリデーションに失敗すると、marshmallow.exceptions.ValidationError 例外が発生します。この例外オブジェクトの messages 属性には、どのフィールドでどのようなエラーが発生したかの詳細情報が辞書形式で格納されています。

from marshmallow import ValidationError, validate

invalid_data = {
    "username": "us", # validate.Length(min=3) に違反
    "email": "invalid-email", # fields.Email の形式に違反
    "age": -5 # validate.Range(min=0) に違反
}

schema = UserSchema() # @post_load がないスキーマを使用

try:
    schema.load(invalid_data)
except ValidationError as err:
    print("--- バリデーションエラー詳細 ---")
    pprint(err.messages)
    # 出力例:
    # {'age': ['Must be greater than or equal to 0.'],
    #  'email': ['Not a valid email address.'],
    #  'username': ['Shorter than minimum length 3.']}

    # バリデーションを通過したデータ(部分的なデータ)も取得可能
    print("\n--- バリデーション通過データ ---")
    pprint(err.valid_data)
    # 出力例: {} (今回は全てエラーなので空)

カスタムバリデーション

組み込みバリデータだけでは不十分な場合、独自のバリデーションロジックを実装できます。

  1. バリデーション関数: 単一の値を受け取り、バリデーションエラー時に ValidationError を発生させるか False を返す関数を作成し、validate 引数に渡します。
  2. バリデーションメソッド: スキーマクラス内に @validates("フィールド名") デコレータを使ってメソッドを定義します。メソッドはフィールドの値を受け取り、エラー時に ValidationError を発生させます。一つのフィールドに複数のバリデーションメソッドを定義することも可能です。
  3. スキーマレベルバリデーション: 特定のフィールドだけでなく、複数のフィールド値の関係性を検証したい場合は、@validates_schema デコレータを使います。このメソッドは、デシリアライズされたデータ全体(辞書)を受け取ります。
from marshmallow import Schema, fields, validates, validates_schema, ValidationError, validate

# 1. バリデーション関数
def must_not_be_admin(value):
    if value.lower() == "admin":
        raise ValidationError("Username 'admin' is not allowed.")

# 2 & 3. バリデーションメソッドとスキーマレベルバリデーション
class RegistrationSchema(Schema):
    username = fields.Str(required=True, validate=[validate.Length(min=4), must_not_be_admin])
    password = fields.Str(required=True, validate=validate.Length(min=8))
    confirm_password = fields.Str(required=True, load_only=True) # デシリアライズ時のみ必要

    # 2. フィールド 'password' に対するバリデーションメソッド
    @validates("password")
    def validate_password_strength(self, value):
        if not any(char.isdigit() for char in value):
            raise ValidationError("Password must contain at least one digit.")
        if not any(char.isupper() for char in value):
            raise ValidationError("Password must contain at least one uppercase letter.")

    # 3. スキーマレベルバリデーション
    @validates_schema
    def validate_passwords_match(self, data, **kwargs):
        if data.get("password") != data.get("confirm_password"):
            # 特定のフィールドに関連付けることも、全体のエラーとすることも可能
            raise ValidationError("Passwords do not match.", field_name="confirm_password")

# テストデータ
valid_reg_data = {
    "username": "testuser123",
    "password": "Password123",
    "confirm_password": "Password123"
}
invalid_reg_data1 = { # パスワード不一致
    "username": "testuser123",
    "password": "Password123",
    "confirm_password": "Password456"
}
invalid_reg_data2 = { # ユーザー名とパスワード強度違反
    "username": "admin",
    "password": "password",
    "confirm_password": "password"
}

reg_schema = RegistrationSchema()

print("\n--- 有効なデータ ---")
try:
    result = reg_schema.load(valid_reg_data)
    pprint(result) # confirm_passwordはload_onlyなので結果に含まれない
    # {'username': 'testuser123', 'password': 'Password123'}
except ValidationError as err:
    pprint(err.messages)

print("\n--- 無効なデータ 1 (パスワード不一致) ---")
try:
    reg_schema.load(invalid_reg_data1)
except ValidationError as err:
    pprint(err.messages)
    # {'confirm_password': ['Passwords do not match.']}

print("\n--- 無効なデータ 2 (ユーザー名&パスワード強度) ---")
try:
    reg_schema.load(invalid_reg_data2)
except ValidationError as err:
    pprint(err.messages)
    # {'username': ["Username 'admin' is not allowed."],
    #  'password': ['Password must contain at least one digit.',
    #               'Password must contain at least one uppercase letter.'],
    #  'confirm_password': ['Passwords do not match.']} # パスワードも一致しない

このように、フィールドごと、あるいはスキーマ全体で柔軟なバリデーションルールを定義できます。

応用的な使い方 発展編 🚀

1. ネストしたスキーマ

実際のアプリケーションでは、データ構造がネストしていることがよくあります。例えば、ブログ記事 (Post) が著者 (Author) 情報を持っている場合などです。Marshmallowでは fields.Nested を使ってスキーマをネストさせることができます。

from marshmallow import Schema, fields, pprint

# まず著者スキーマを定義
class AuthorSchema(Schema):
    id = fields.Int(dump_only=True)
    name = fields.Str(required=True)
    email = fields.Email()

# 記事スキーマを定義し、著者フィールドで Nested を使用
class PostSchema(Schema):
    id = fields.Int(dump_only=True)
    title = fields.Str(required=True)
    content = fields.Str()
    # AuthorSchema をネスト
    author = fields.Nested(AuthorSchema, required=True)
    created_at = fields.DateTime(dump_only=True)

# サンプルデータ (オブジェクト)
class Author:
    def __init__(self, id, name, email=None):
        self.id = id
        self.name = name
        self.email = email

class Post:
    def __init__(self, id, title, author, content=None, created_at=None):
        self.id = id
        self.title = title
        self.author = author
        self.content = content
        self.created_at = created_at or datetime.datetime.now()

author_obj = Author(id=101, name="Alice", email="alice@example.com")
post_obj = Post(id=1, title="My First Post", author=author_obj, content="This is the content.")

# シリアライズ (dump)
post_schema = PostSchema()
serialized_post = post_schema.dump(post_obj)
pprint(serialized_post)
# 出力例:
# {'id': 1,
#  'title': 'My First Post',
#  'content': 'This is the content.',
#  'author': {'id': 101, 'name': 'Alice', 'email': 'alice@example.com'},
#  'created_at': '...'}

# サンプルデータ (辞書)
input_post_data = {
    "title": "Another Post",
    "content": "More content here.",
    "author": { # ネストしたデータ
        "name": "Bob" # emailは必須ではない
        # id は dump_only なので不要
    }
}

# デシリアライズ (load)
try:
    deserialized_post = post_schema.load(input_post_data)
    pprint(deserialized_post)
    # 出力例 (デフォルトでは辞書):
    # {'title': 'Another Post',
    #  'content': 'More content here.',
    #  'author': {'name': 'Bob', 'email': None}} # email はデフォルトNone
except ValidationError as err:
    pprint(err.messages)

fields.Nested(NestedSchema) のように、ネストさせたいスキーマクラスを第一引数に渡します。これにより、シリアライズ時にはネストされたオブジェクトが対応するスキーマでシリアライズされ、デシリアライズ時にはネストされたデータが対応するスキーマでデシリアライズ・バリデーションされます。

@post_load を使って、ネストされたデータも含むオブジェクトを生成することも可能です。その場合、ネストされたスキーマ (AuthorSchema) にも @post_load を定義しておくと、PostSchema@post_load メソッドは、すでに Author オブジェクトが生成された状態のデータを受け取ることができます。

2. コンテキストの利用

スキーマのシリアライズやデシリアライズの際に、外部から追加の情報(コンテキスト)を渡したい場合があります。例えば、現在のリクエスト情報やユーザー情報などです。スキーマのインスタンス化時や dump() / load() メソッド呼び出し時に context 引数を使用します。

コンテキストは、スキーマ内のメソッド(@pre_load, @post_load, @validates, @validates_schema やカスタムフィールドのメソッドなど)から self.context でアクセスできます。

from marshmallow import Schema, fields, validates_schema, ValidationError

class CommentSchema(Schema):
    id = fields.Int(dump_only=True)
    text = fields.Str(required=True)
    user_id = fields.Int(required=True, load_only=True) # コメントしたユーザーのID

    @validates_schema
    def check_user_permission(self, data, **kwargs):
        # コンテキストから現在のユーザーIDを取得
        current_user_id = self.context.get("user_id")
        if not current_user_id:
            raise ValidationError("Authentication required to post comments.")
        # ここでは例として、自分のコメント以外は編集できない、などのロジックを実装できる
        # (この例では単純化のため、コンテキストにuser_idがあるかだけチェック)
        print(f"Context user_id: {current_user_id}")

# コンテキストを渡してスキーマをインスタンス化
schema_with_context = CommentSchema(context={"user_id": 123})

comment_data = {"text": "This is a comment.", "user_id": 456} # 別のユーザーのコメント

try:
    # dump や load 時に context を渡すことも可能
    # result = schema.load(comment_data, context={"user_id": 123})
    result = schema_with_context.load(comment_data)
    pprint(result)
    # Context user_id: 123
    # {'text': 'This is a comment.'}
except ValidationError as err:
    pprint(err.messages)

# コンテキストなしで実行してみる
schema_without_context = CommentSchema()
try:
    schema_without_context.load(comment_data)
except ValidationError as err:
    print("\n--- コンテキストなしの場合のエラー ---")
    pprint(err.messages)
    # {'_schema': ['Authentication required to post comments.']}

コンテキストを使うことで、スキーマ自体に状態を持たせることなく、外部の情報に基づいて動的なバリデーションやデータ処理を行うことが可能になります。

3. フィールドのカスタマイズ属性

フィールド定義時には、required, dump_only, load_only, validate, load_default, dump_default 以外にも多くのカスタマイズ用引数があります。

属性 説明
data_key シリアライズ/デシリアライズ時のキー名をPython属性名と変えたい場合に指定。 created_at = fields.DateTime(data_key="creationDate")
attribute オブジェクトから値を取得する際の属性名をフィールド名と変えたい場合に指定。 user_name = fields.Str(attribute="username")
allow_none True の場合、None 値を許可する(デフォルトは False)。required=True と併用可能で、キーは必須だが値は None でも良い、という状況を表せる。 email = fields.Email(allow_none=True)
load_only True の場合、デシリアライズ時のみ有効で、シリアライズ時には無視される。パスワードの確認入力などに使う。 password_confirm = fields.Str(load_only=True)
dump_only True の場合、シリアライズ時のみ有効で、デシリアライズ時には無視される。DBが自動生成するIDなどに使う。 id = fields.Int(dump_only=True)
missing デシリアライズ時に入力データにキーが存在しない場合のデフォルト値。load_default と似ているが、missing は値が存在しない場合に適用され、load_default はキーが存在しても値が None (または指定したセンチネル値) の場合に適用される(挙動はフィールド型による)。load_default の方が推奨されることが多い。 status = fields.Str(missing="pending")
default シリアライズ時にオブジェクトの属性値が None (または指定したセンチネル値) の場合のデフォルト値。dump_default とも呼ばれる。 role = fields.Str(default="guest")
error_messages バリデーションエラー時のメッセージをカスタマイズするための辞書。 name = fields.Str(required=True, error_messages={"required": "名前は必須です。"})

これらの属性を組み合わせることで、より細やかなデータ変換とバリデーション制御が可能になります。特に data_key は、Pythonの命名規則 (snake_case) とJSON APIの一般的な命名規則 (camelCase) を変換する際によく利用されます。

4. スキーマの継承と `Schema.Meta` オプション

スキーマ定義もクラスなので、Pythonの継承を利用できます。共通のフィールドを持つスキーマがある場合、ベースとなるスキーマを定義し、それを継承して特定のフィールドを追加・変更することができます。

また、Schema.Meta クラスを使って、スキーマ全体の挙動を制御するオプションを設定できます。

from marshmallow import Schema, fields, INCLUDE, EXCLUDE

# ベーススキーマ
class BaseAuditSchema(Schema):
    created_at = fields.DateTime(dump_only=True)
    updated_at = fields.DateTime(dump_only=True)

    class Meta:
        ordered = True # 出力順を定義順にする

# BaseAuditSchemaを継承したユーザースキーマ
class DetailedUserSchema(BaseAuditSchema):
    id = fields.Int(dump_only=True)
    username = fields.Str(required=True)
    email = fields.Email(required=True)
    profile = fields.Nested("ProfileSchema") # 文字列で指定することも可能 (循環参照対策)

    class Meta(BaseAuditSchema.Meta): # Metaも継承できる
        # unknown=INCLUDE: スキーマに定義されていないフィールドも結果に含める
        # unknown=EXCLUDE: スキーマに定義されていないフィールドは無視する (デフォルト)
        # unknown=RAISE: スキーマに定義されていないフィールドがあるとValidationError
        unknown = INCLUDE

class ProfileSchema(Schema):
    bio = fields.Str()
    website = fields.URL()

# サンプルデータ
user_with_profile = {
    "id": 1,
    "username": "detail_user",
    "email": "detail@example.com",
    "profile": {
        "bio": "I love Python!",
        "website": "https://example.com"
    },
    "created_at": datetime.datetime.now(),
    "updated_at": datetime.datetime.now(),
    "extra_field": "This is unknown" # Metaでunknown=INCLUDEを指定したので含まれる
}

detailed_schema = DetailedUserSchema()
result = detailed_schema.dump(user_with_profile)
pprint(result)
# 出力例 (ordered=True なので定義順に近い + unknown=INCLUDE で extra_field が含まれる):
# OrderedDict([('created_at', '...'),
#              ('updated_at', '...'),
#              ('id', 1),
#              ('username', 'detail_user'),
#              ('email', 'detail@example.com'),
#              ('profile', OrderedDict([('bio', 'I love Python!'),
#                                       ('website', 'https://example.com')])),
#              ('extra_field', 'This is unknown')])

Schema.Meta で設定可能な主なオプション:

  • fields: 出力するフィールドのタプル(only と似ているが Meta で定義)
  • exclude: 除外するフィールドのタプル
  • ordered: True の場合、出力時のフィールド順を定義順にする
  • unknown: 未知のフィールド(スキーマに定義されていない)をどう扱うか (INCLUDE, EXCLUDE, RAISE)
  • dateformat: DateTimeフィールドのデフォルトのシリアライズ形式
  • load_instance: True の場合、load メソッドがオブジェクトを直接変更しようとする(通常は非推奨、@post_load を使う)
  • render_module: dumpsloads で使用するJSONライブラリ (デフォルトは json, ujson なども指定可能)
  • register: スキーマを内部レジストリに登録するかどうか(Nestedで文字列参照する場合に必要、デフォルトTrue

5. フックメソッド (`@pre_load`, `@post_dump` など)

@post_load 以外にも、シリアライズ/デシリアライズの特定のタイミングで処理を挟むためのデコレータ(フックメソッド)が用意されています。

  • @pre_load: load が呼ばれた直後、バリデーション前に実行。入力データの前処理に使う。
  • @post_load: バリデーションと型変換後、最終的なオブジェクト生成前に実行。(既に説明済み)
  • @pre_dump: dump が呼ばれた直後、シリアライズ前に実行。シリアライズ対象オブジェクトの前処理に使う。
  • @post_dump: シリアライズ後、最終的なデータが出力される前に実行。シリアライズ結果の後処理に使う。
from marshmallow import Schema, fields, pre_load, post_dump, pre_dump

class EnvelopeSchema(Schema):
    data = fields.Nested("DataSchema")
    status = fields.Str(default="success")
    request_id = fields.Str(dump_only=True)

    class Meta:
        ordered = True

    # pre_dump: オブジェクト -> 辞書 変換前に実行
    @pre_dump
    def add_request_id_to_object(self, obj, **kwargs):
        # 例: コンテキストからリクエストIDを取得してオブジェクトに追加(ここではダミー)
        # obj.request_id = self.context.get("request_id", "unknown")
        # ただし、元のオブジェクトを変更するのは推奨されない場合もある
        # dumpするデータに含めるだけなら @post_dump の方が適していることも
        print("Executing pre_dump")
        return obj # 変更したオブジェクト(または元のオブジェクト)を返す

    # post_dump: 辞書 -> 最終出力 変換前に実行
    @post_dump
    def wrap_with_envelope(self, data, **kwargs):
        print("Executing post_dump")
        # data は {'data': {...}, 'status': 'success'} のような辞書
        # ここでさらに加工できる
        envelope = {
            "response": data,
            "server_time": datetime.datetime.now().isoformat()
        }
        return envelope # 最終的に出力されるデータを返す

class DataSchema(Schema):
    message = fields.Str()

    # pre_load: 入力辞書 -> バリデーション前 に実行
    @pre_load
    def preprocess_input(self, in_data, **kwargs):
        print("Executing pre_load")
        # 例: 入力キーを小文字に変換するなど
        return {k.lower(): v for k, v in in_data.items()}


# --- 実行例 ---
data_obj = {"message": "Hello!"} # 本来はクラスインスタンスなど

# シリアライズ (dump)
envelope_schema = EnvelopeSchema()
result_dump = envelope_schema.dump({"data": data_obj}) # dataフィールドに渡すオブジェクト
pprint(result_dump)
# Executing pre_dump
# Executing post_dump
# {'response': OrderedDict([('data', {'message': 'Hello!'}), ('status', 'success')]),
#  'server_time': '...'}


# デシリアライズ (load)
input_envelope = {
    "DATA": { # 大文字キー
        "MESSAGE": "Input Message"
    }
    # status はなくてもデフォルト値が使われる
}
try:
    result_load = envelope_schema.load(input_envelope)
    pprint(result_load)
    # Executing pre_load # DataSchema の pre_load が呼ばれる
    # {'data': {'message': 'Input Message'}, 'status': 'success'}
except ValidationError as err:
    pprint(err.messages)

これらのフックメソッドを使いこなすことで、データの変換プロセスをより柔軟に制御できます。例えば、APIレスポンスを特定の形式(エンベロープ形式など)でラップしたり、入力データに含まれる不要なキーを削除したりといった処理が可能です。

ユースケース 💡

Marshmallowは様々な場面で活躍します。

  • Web API開発 (Flask, Djangoなど):
    • APIリクエストボディのバリデーションとオブジェクトへのデシリアライズ。
    • データベースから取得したモデルオブジェクトをAPIレスポンス用のJSON形式へシリアライズ。
    • Flaskには Flask-Marshmallow、Djangoには django-rest-marshmallow といった連携ライブラリがあり、より簡単に統合できます。これらはURL生成フィールドなども提供します。
  • 設定ファイルの読み込み/書き出し:
    • YAMLやJSON形式の設定ファイルを読み込み、バリデーションし、Pythonオブジェクトとして扱う。
    • アプリケーションの設定オブジェクトをファイルに書き出す。
  • データ変換パイプライン:
    • 異なるシステム間でデータをやり取りする際に、フォーマット変換とバリデーションを行う。
    • CSVファイルやデータベースから読み込んだデータを、特定の構造を持つオブジェクトに変換する。
  • ORM/ODMとの連携:
    • SQLAlchemyやMongoEngineなどのORM/ODMのモデルオブジェクトと、シリアライズ/デシリアライズ可能なデータ形式(辞書やJSON)との間の変換。モデルの構造をスキーマ定義に反映させるヘルパー機能を持つライブラリもあります (例: marshmallow-sqlalchemy)。

まとめ 🎉

Marshmallowは、Pythonにおけるデータバリデーション、シリアライズ、デシリアライズのための非常に強力で柔軟なライブラリです。スキーマを定義することで、データの構造とルールを一元管理し、コードの可読性、再利用性、堅牢性を大幅に向上させることができます。

主なメリットを再確認しましょう:

  • 宣言的なスキーマ定義: データの構造とルールを明確に記述できます。
  • 強力なバリデーション: 組み込みバリデータとカスタムバリデーションで、データの整合性を保証します。
  • 🔄 簡単なシリアライズ/デシリアライズ: オブジェクトとデータ形式(辞書、JSONなど)の相互変換を容易にします。
  • 🧩 高い拡張性: カスタムフィールド、フックメソッド、コンテキストなどにより、複雑な要件にも対応できます。
  • 🌐 フレームワーク非依存: 特定のWebフレームワークやORMに縛られず、様々なプロジェクトで利用できます(連携ライブラリも豊富)。

この記事では基本的な使い方から応用的なテクニックまで幅広く解説しましたが、Marshmallowにはさらに多くの機能があります。ぜひ公式ドキュメント (https://marshmallow.readthedocs.io/) を参照し、あなたのPythonプロジェクトでMarshmallowを活用してみてください!きっと開発効率が向上するはずです。🚀