Pythonでのデータ処理を劇的に効率化するMarshmallowの全てを学びましょう!
Marshmallowとは? 🤔
Marshmallowは、Pythonで複雑なデータ型(オブジェクトなど)とネイティブなPythonデータ型(辞書、リストなど)の間で相互変換を行うための、ORM/ODM/フレームワーク非依存のライブラリです。 主に以下の3つの強力な機能を提供します。
- 入力データのバリデーション(検証): APIリクエストやフォーム入力など、外部から受け取ったデータが正しい形式・値を持っているかを確認します。
- デシリアライズ: JSON文字列やPython辞書などのデータを、アプリケーションで扱いやすいオブジェクトに変換します。
- シリアライズ: アプリケーション内のオブジェクトを、JSONなどの標準的な形式で出力するために、プリミティブなPythonデータ型(辞書など)に変換します。
これにより、API開発、設定ファイルの読み書き、データ変換処理など、様々な場面でコードの記述量を削減し、堅牢性と可読性を向上させることができます。特にWebフレームワーク(FlaskやDjangoなど)との親和性が高く、APIのエンドポイントでリクエストデータを受け取り、レスポンスデータを返す際の処理を非常に簡潔に記述できます。👍
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
が指定された id
と created_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)
# 出力例: {} (今回は全てエラーなので空)
カスタムバリデーション
組み込みバリデータだけでは不十分な場合、独自のバリデーションロジックを実装できます。
- バリデーション関数: 単一の値を受け取り、バリデーションエラー時に
ValidationError
を発生させるかFalse
を返す関数を作成し、validate
引数に渡します。 - バリデーションメソッド: スキーマクラス内に
@validates("フィールド名")
デコレータを使ってメソッドを定義します。メソッドはフィールドの値を受け取り、エラー時にValidationError
を発生させます。一つのフィールドに複数のバリデーションメソッドを定義することも可能です。 - スキーマレベルバリデーション: 特定のフィールドだけでなく、複数のフィールド値の関係性を検証したい場合は、
@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
:dumps
やloads
で使用する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)。
from flask import Flask, request, jsonify
from flask_sqlalchemy import SQLAlchemy
from flask_marshmallow import Marshmallow # Flask-Marshmallow をインポート
app = Flask(__name__)
# (データベース設定など...)
db = SQLAlchemy(app)
ma = Marshmallow(app) # MarshmallowをFlaskアプリに紐付け
# SQLAlchemyモデル (例)
class Product(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), unique=True, nullable=False)
price = db.Column(db.Float, nullable=False)
# Marshmallowスキーマ (Flask-Marshmallowの機能を利用)
class ProductSchema(ma.SQLAlchemyAutoSchema): # SQLAlchemyモデルから自動生成
class Meta:
model = Product
load_instance = True # デシリアライズ時にモデルインスタンスを生成/更新
product_schema = ProductSchema()
products_schema = ProductSchema(many=True)
@app.route('/products', methods=['POST'])
def add_product():
try:
# リクエストJSONをデシリアライズ&バリデーションし、Productインスタンスを生成
new_product = product_schema.load(request.json)
db.session.add(new_product)
db.session.commit()
# 生成されたProductインスタンスをシリアライズして返す
return product_schema.jsonify(new_product), 201
except ValidationError as err:
return jsonify(err.messages), 400
@app.route('/products', methods=['GET'])
def get_products():
all_products = Product.query.all()
# Productインスタンスのリストをシリアライズ
result = products_schema.dump(all_products)
return jsonify(result)
@app.route('/products/<int:id>', methods=['GET'])
def get_product(id):
product = Product.query.get_or_404(id)
# 単一のProductインスタンスをシリアライズ
return product_schema.jsonify(product)
# (PUT, DELETEなどのエンドポイントも同様に実装)
if __name__ == '__main__':
# 初回実行時などにテーブル作成
with app.app_context():
db.create_all()
app.run(debug=True)
Flask-Marshmallowとmarshmallow-sqlalchemy (SQLAlchemyAutoSchema
はその一部) を使うと、SQLAlchemyモデルからスキーマを半自動生成でき、コード量をさらに削減できます。
まとめ 🎉
Marshmallowは、Pythonにおけるデータバリデーション、シリアライズ、デシリアライズのための非常に強力で柔軟なライブラリです。スキーマを定義することで、データの構造とルールを一元管理し、コードの可読性、再利用性、堅牢性を大幅に向上させることができます。
主なメリットを再確認しましょう:
- ✨ 宣言的なスキーマ定義: データの構造とルールを明確に記述できます。
- ✅ 強力なバリデーション: 組み込みバリデータとカスタムバリデーションで、データの整合性を保証します。
- 🔄 簡単なシリアライズ/デシリアライズ: オブジェクトとデータ形式(辞書、JSONなど)の相互変換を容易にします。
- 🧩 高い拡張性: カスタムフィールド、フックメソッド、コンテキストなどにより、複雑な要件にも対応できます。
- 🌐 フレームワーク非依存: 特定のWebフレームワークやORMに縛られず、様々なプロジェクトで利用できます(連携ライブラリも豊富)。
この記事では基本的な使い方から応用的なテクニックまで幅広く解説しましたが、Marshmallowにはさらに多くの機能があります。ぜひ公式ドキュメント (https://marshmallow.readthedocs.io/) を参照し、あなたのPythonプロジェクトでMarshmallowを活用してみてください!きっと開発効率が向上するはずです。🚀