Schema

シンプルで強力なPythonデータ検証ライブラリ

Schemaは、Pythonのデータ構造を検証するためのシンプルかつ強力なライブラリです。複雑なネストされたデータ構造の検証を、直感的で読みやすい方法で実現します。

主な特徴

  • シンプルなAPI: Pythonの型システムを活用した直感的な検証定義
  • 柔軟な検証: カスタム検証関数による高度な検証ロジック
  • 詳細なエラーメッセージ: デバッグに役立つ明確なエラー情報
  • 軽量: 外部依存なしの単一ファイルライブラリ
  • Python互換: Python 2.7から最新版まで幅広くサポート

インストール

pipを使用してSchemaをインストールします:

pip install schema

基本的な使い方

シンプルなスキーマ定義

from schema import Schema

# 基本的なスキーマ定義
user_schema = Schema({
    'name': str,
    'age': int,
    'email': str
})

# データの検証
data = {
    'name': '田中太郎',
    'age': 30,
    'email': '[email protected]'
}

validated_data = user_schema.validate(data)
print(validated_data)

型の検証

from schema import Schema

# 様々な型の検証
schema = Schema({
    'string_field': str,
    'int_field': int,
    'float_field': float,
    'bool_field': bool,
    'list_field': list,
    'dict_field': dict
})

data = {
    'string_field': 'テキスト',
    'int_field': 42,
    'float_field': 3.14,
    'bool_field': True,
    'list_field': [1, 2, 3],
    'dict_field': {'key': 'value'}
}

validated = schema.validate(data)

高度な検証

オプショナルフィールド

from schema import Schema, Optional

# オプショナルフィールドを含むスキーマ
product_schema = Schema({
    'name': str,
    'price': float,
    Optional('description'): str,
    Optional('tags'): [str]
})

# descriptionとtagsは省略可能
data1 = {
    'name': 'ノートPC',
    'price': 120000.0
}

data2 = {
    'name': 'マウス',
    'price': 3000.0,
    'description': 'ワイヤレスマウス',
    'tags': ['周辺機器', 'ワイヤレス']
}

# 両方とも有効
validated1 = product_schema.validate(data1)
validated2 = product_schema.validate(data2)

リストとタプルの検証

from schema import Schema, And

# リストの要素を検証
numbers_schema = Schema([int])

# タプルの検証
coordinate_schema = Schema((float, float))

# 条件付きリスト検証
positive_numbers = Schema([And(int, lambda n: n > 0)])

# 使用例
numbers = numbers_schema.validate([1, 2, 3, 4, 5])
coords = coordinate_schema.validate((35.6762, 139.6503))  # 東京の座標
positive = positive_numbers.validate([10, 20, 30])

カスタム検証関数

from schema import Schema, And, Use

# メールアドレスの検証
import re

def is_email(email):
    pattern = r'^[\w\.-]+@[\w\.-]+\.\w+$'
    if not re.match(pattern, email):
        raise ValueError(f'{email} は有効なメールアドレスではありません')
    return email

# 年齢の範囲検証
def valid_age(age):
    if not 0 <= age <= 120:
        raise ValueError(f'年齢は0〜120の範囲で入力してください')
    return age

# カスタム検証を含むスキーマ
user_schema = Schema({
    'name': And(str, len),  # 空文字列を許可しない
    'email': And(str, is_email),
    'age': And(int, valid_age),
    'phone': And(str, lambda p: re.match(r'^\d{3}-\d{4}-\d{4}$', p))
})

# 検証例
data = {
    'name': '山田花子',
    'email': '[email protected]',
    'age': 25,
    'phone': '090-1234-5678'
}

validated = user_schema.validate(data)

複雑なネスト構造

from schema import Schema, Optional

# 企業情報のスキーマ
company_schema = Schema({
    'name': str,
    'founded': int,
    'employees': And(int, lambda n: n > 0),
    'address': {
        'street': str,
        'city': str,
        'prefecture': str,
        'postal_code': And(str, lambda p: re.match(r'^\d{3}-\d{4}$', p))
    },
    'departments': [{
        'name': str,
        'manager': str,
        'staff_count': int
    }],
    Optional('website'): And(str, lambda u: u.startswith('http'))
})

# サンプルデータ
company_data = {
    'name': '株式会社サンプル',
    'founded': 2010,
    'employees': 150,
    'address': {
        'street': '東京都千代田区丸の内1-1-1',
        'city': '千代田区',
        'prefecture': '東京都',
        'postal_code': '100-0001'
    },
    'departments': [
        {
            'name': '開発部',
            'manager': '佐藤太郎',
            'staff_count': 50
        },
        {
            'name': '営業部',
            'manager': '鈴木花子',
            'staff_count': 30
        }
    ],
    'website': 'https://example.co.jp'
}

validated = company_schema.validate(company_data)

値の変換

from schema import Schema, Use

# 値の自動変換
conversion_schema = Schema({
    'price': Use(float),  # 文字列を浮動小数点に変換
    'quantity': Use(int),  # 文字列を整数に変換
    'date': Use(lambda d: datetime.strptime(d, '%Y-%m-%d')),
    'tags': Use(lambda t: [tag.strip() for tag in t.split(',')])
})

# 入力データ
raw_data = {
    'price': '1500.50',
    'quantity': '10',
    'date': '2024-01-15',
    'tags': 'Python, 検証, ライブラリ'
}

# 変換と検証
from datetime import datetime
converted = conversion_schema.validate(raw_data)
print(converted)
# {'price': 1500.5, 'quantity': 10, 'date': datetime.datetime(2024, 1, 15, 0, 0), 'tags': ['Python', '検証', 'ライブラリ']}

エラーハンドリング

from schema import Schema, SchemaError, And

# 厳密なスキーマ
strict_schema = Schema({
    'name': And(str, len),
    'age': And(int, lambda a: 18 <= a <= 65),
    'email': And(str, lambda e: '@' in e)
})

# エラーハンドリング
try:
    invalid_data = {
        'name': '',  # 空文字列
        'age': 16,   # 範囲外
        'email': 'invalid-email'  # @がない
    }
    strict_schema.validate(invalid_data)
except SchemaError as e:
    print(f"検証エラー: {e}")
    # エラーの詳細を取得
    print(f"エラーの詳細: {e.args[0]}")

カスタムエラーメッセージ

from schema import Schema, And, SchemaError

def validate_japanese_phone(phone):
    """日本の電話番号形式を検証"""
    if not re.match(r'^0\d{1,4}-\d{1,4}-\d{4}$', phone):
        raise SchemaError('電話番号は日本の形式(例:03-1234-5678)で入力してください')
    return phone

def validate_postal_code(code):
    """郵便番号形式を検証"""
    if not re.match(r'^\d{3}-\d{4}$', code):
        raise SchemaError('郵便番号は xxx-xxxx の形式で入力してください')
    return code

# カスタムエラーメッセージ付きスキーマ
contact_schema = Schema({
    'phone': validate_japanese_phone,
    'postal_code': validate_postal_code
})

# エラーメッセージの確認
try:
    contact_schema.validate({
        'phone': '0312345678',  # ハイフンなし
        'postal_code': '1234567'  # 形式が違う
    })
except SchemaError as e:
    print(e)  # カスタムエラーメッセージが表示される

実践的な使用例

APIレスポンスの検証

from schema import Schema, Optional, Or

# APIレスポンスのスキーマ
api_response_schema = Schema({
    'status': Or('success', 'error'),
    'data': Or(dict, None),
    Optional('error'): {
        'code': int,
        'message': str
    },
    'timestamp': str
})

# 成功レスポンスの検証
success_response = {
    'status': 'success',
    'data': {
        'user_id': 123,
        'username': 'tanaka'
    },
    'timestamp': '2024-01-15T10:30:00Z'
}

# エラーレスポンスの検証
error_response = {
    'status': 'error',
    'data': None,
    'error': {
        'code': 404,
        'message': 'ユーザーが見つかりません'
    },
    'timestamp': '2024-01-15T10:31:00Z'
}

# 両方とも有効
validated_success = api_response_schema.validate(success_response)
validated_error = api_response_schema.validate(error_response)

設定ファイルの検証

from schema import Schema, Optional, And

# アプリケーション設定のスキーマ
config_schema = Schema({
    'app': {
        'name': str,
        'version': And(str, lambda v: re.match(r'^\d+\.\d+\.\d+$', v)),
        'debug': bool
    },
    'database': {
        'host': str,
        'port': And(int, lambda p: 1 <= p <= 65535),
        'name': str,
        Optional('username'): str,
        Optional('password'): str
    },
    'logging': {
        'level': Or('DEBUG', 'INFO', 'WARNING', 'ERROR'),
        'file': str,
        Optional('max_size'): And(int, lambda s: s > 0)
    }
})

# 設定データ
config = {
    'app': {
        'name': 'MyApplication',
        'version': '1.2.3',
        'debug': False
    },
    'database': {
        'host': 'localhost',
        'port': 5432,
        'name': 'myapp_db',
        'username': 'dbuser'
    },
    'logging': {
        'level': 'INFO',
        'file': 'app.log',
        'max_size': 1048576  # 1MB
    }
}

validated_config = config_schema.validate(config)

ベストプラクティス

1. スキーマの再利用

# 共通スキーマの定義
from schema import Schema, And

# 再利用可能なバリデータ
positive_int = And(int, lambda n: n > 0)
email_validator = And(str, lambda e: re.match(r'^[\w\.-]+@[\w\.-]+\.\w+$', e))
japanese_phone = And(str, lambda p: re.match(r'^0\d{1,4}-\d{1,4}-\d{4}$', p))

# 住所スキーマ
address_schema = {
    'postal_code': And(str, lambda p: re.match(r'^\d{3}-\d{4}$', p)),
    'prefecture': str,
    'city': str,
    'street': str
}

# ユーザースキーマで再利用
user_schema = Schema({
    'id': positive_int,
    'email': email_validator,
    'phone': japanese_phone,
    'address': address_schema
})

# 企業スキーマでも再利用
company_schema = Schema({
    'id': positive_int,
    'contact_email': email_validator,
    'headquarters': address_schema
})

2. エラーメッセージの国際化

from schema import Schema, SchemaError

class JapaneseSchemaError(SchemaError):
    """日本語エラーメッセージ用のカスタム例外"""
    pass

def create_validator(error_message):
    """エラーメッセージ付きバリデータを作成"""
    def validator(func):
        def wrapper(value):
            try:
                return func(value)
            except:
                raise JapaneseSchemaError(error_message)
        return wrapper
    return validator

@create_validator('年齢は0以上120以下で入力してください')
def validate_age(age):
    if not 0 <= age <= 120:
        raise ValueError()
    return age

@create_validator('有効なメールアドレスを入力してください')
def validate_email(email):
    if not re.match(r'^[\w\.-]+@[\w\.-]+\.\w+$', email):
        raise ValueError()
    return email

# 使用例
schema = Schema({
    'age': And(int, validate_age),
    'email': validate_email
})

3. パフォーマンスの最適化

from schema import Schema
import functools

# スキーマをキャッシュして再利用
@functools.lru_cache(maxsize=128)
def get_user_schema():
    return Schema({
        'id': int,
        'name': str,
        'email': str,
        'created_at': str
    })

# 大量のデータを検証する場合
def validate_users_batch(users_data):
    schema = get_user_schema()
    validated_users = []
    errors = []
    
    for i, user in enumerate(users_data):
        try:
            validated = schema.validate(user)
            validated_users.append(validated)
        except SchemaError as e:
            errors.append({
                'index': i,
                'error': str(e),
                'data': user
            })
    
    return validated_users, errors

まとめ

Schemaは、Pythonアプリケーションでデータ検証を行うための強力で柔軟なツールです。シンプルなAPIと豊富な機能により、基本的な型チェックから複雑なビジネスロジックの検証まで、幅広いユースケースに対応できます。特に日本のプロジェクトでは、郵便番号や電話番号などの地域固有の形式を簡単に検証できる点が大きな利点となります。