Pythonの汎用データバリデーションライブラリ「Cerberus」を使う

バリデーションチェック、めんどくさいですよね。

最初は[FormEncode](http://www.formencode.org/en/latest/)でやろうとしたのですが、「もうちょっと今っぽいやつないのかな……」と探したところ、とてもいい感じのライブラリがあったので、使い方をメモしておきます。ご参考になれば幸いです。

# Cerberusとは

* [GitHub \- nicolaiarocci/cerberus: Lightweight, extensible data validation library for Python](https://github.com/nicolaiarocci/cerberus)

英語読みで「サーベラス」でいいんでしょうか？　いわゆる「ケルベロス」のことで、「冥界の番犬」のようにデータの入り口を守る、というのが名前の由来のようです。~~厨ニっぽい~~ かっこいいですね。 


# 導入

```
$ pip install cerberus
```

# 使い方

サンプル見て頂くのが早いです。こんな感じに今っぽく書けます。
(Python 3.5.2 で動作確認しています)

```python:sample_cerberus.py
# -*- coding: utf-8 -*-

from cerberus import Validator

import re
from datetime import datetime, date
from pprint import pprint

# バリデーション定義
schema = {
	'name': {
        'type': 'string',
        'required': True,
        'empty': False,
    },
    'email': {
        'type': 'string',
        'required': True,
        'regex': '^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$',
    },
	'age': {
		'type': 'integer',
		'min': 0,
	},
    'phones': {
        'type': 'list',
        'schema': {
            'type': 'string',
            'regex': '^[0-9]{2,4}-[0-9]{2,4}-[0-9]{3,4}$',
        }
    },
	'address': {
        'type': 'string',
        'empty': True,
    },
    'birthday': {
        'type': 'date',
    }
}

# バリデータを作成
v = Validator(schema)

# 入力値1(バリデーションOK)
data_ok = {
    'name': '田中一郎',
    'email': 'tanaka@test.co.jp',
    'age': 30,
    'phones': [
        '012-3456-7890',
        '0120-444-444',
    ],
    'address': '',
    'birthday': date(1990, 7, 7),
}

# 入力値2(バリデーションNG)
data_ng = {
    'name': '',
#    'email': 'tanaka@test.co.jp',
    'age': -300,
    'phones': [
        '01234567890',
        'skype',
    ],
    'address': None,
    'birthday': '1990-07-07',
    'sex': 'male',
}

# バリデーション実施、結果表示
print("-----------------------------------------")
print("◯OK")
print("-----------------------------------------")
pprint(v.validate(data_ok))
pprint(v.errors)

print("-----------------------------------------")
print("×NG")
print("-----------------------------------------")
pprint(v.validate(data_ng))
pprint(v.errors)
```

実行結果

```python
-----------------------------------------
◯OK
-----------------------------------------
True
{}
-----------------------------------------
×NG
-----------------------------------------
False
{'age': ['min value is 0'],
 'birthday': ['must be of date type'],
 'email': ['required field'],
 'name': ['empty values not allowed'],
 'phones': [{0: ['value does not match regex '
                 "'^[0-9]{2,4}-[0-9]{2,4}-[0-9]{3,4}$'"],
             1: ['value does not match regex '
                 "'^[0-9]{2,4}-[0-9]{2,4}-[0-9]{3,4}$'"]}],
 'sex': ['unknown field']}
```

サンプルではよく使いそうなバリデーションルールを中心に取り上げましたが、他にも色々あるので、詳しくは以下をご覧ください。

[Validation Rules — Cerberus is a lightweight and extensible data validation library for Python](http://docs.python-cerberus.org/en/stable/validation-rules.html)

# バリデーションエラーメッセージの日本語化

このままでも使えますが、おおかた「メッセージは日本語にできないの？」って話になりますよね。たぶん。

残念ながら2016/11時点の最新バージョン(v1.0.1)では、リソースファイルなどを使った多言語対応はされていません。ですが、[BasicErrorHandlerのメッセージ定義を上書きすることで](https://github.com/nicolaiarocci/cerberus/blob/master/cerberus/errors.py#L393)、バリデーションエラーメッセージを日本語化することが可能です。

```python
# -*- coding: utf-8 -*-

from pprint import pprint

from cerberus import Validator
from cerberus.errors import BasicErrorHandler

class CustomErrorHandler(BasicErrorHandler):
    """ BasicErrorHandler.message を上書きして日本語化 """
    def __init__(self, tree=None):
        super(CustomErrorHandler, self).__init__(tree)
        # 文言を適宜日本語化してください
        self.messages = {
            0x00: "{0}",
            0x01: "document is missing",
            0x02: "required field",
            0x03: "知らない項目 '{field}' です",
#            ... (略) ...
            0x93: "no definitions validate",
            0x94: "one or more definitions don't validate"
        }


schema = {
    'mail_address' : {'type' : 'string'},
}
data = {
    'name': '田中',     # 0x03: バリデーションエラー
    'mail_address' : 'tanaka_at_test.com',
}

# 拡張したBasicErrorHandlerを指定
v = Validator(schema, error_handler=CustomErrorHandler())

pprint(v.validate(data))
pprint(v.errors)

```

実行結果

```
False
{'name': ["知らない項目 'name' です"]}
```

[こちらにIssueが上がっているので](https://github.com/nicolaiarocci/cerberus/issues/174)、そのうち、こんなことする必要はなくなるかもしれません。なるといいなあ……


# バリデーションルールのカスタマイズ

上記サンプルを見ると、「メールアドレスをregexでチェックしているところ、カスタムバリデーションにできない？」って話になりますよね。たぶん。

[ここにドキュメントがある](http://docs.python-cerberus.org/en/stable/customize.html)のですが、どういうときにどれを使うべきなのかがよくわからなかったので、簡単にまとめておきます。

## 1. カスタムデータ型を追加する (Custom input_data Types)

'type'として独自のデータ型を指定する場合です。

```
schema = {
    'mail_address' : {
        'type' : 'email',
    },
}
```

{'type': 'integer'} とか {'type': 'string'} とか、バリデーションに使えるデータタイプはいくつかありますが、ここに新しく{'type' : 'email'} を追加する、というイメージです。

Validatorを拡張したクラス(`CustomValidator`)を作成、そこにデータタイプを追加します。

```python
# -*- coding: utf-8 -*-

import re
from pprint import pprint

from cerberus import Validator

REGEX_EMAIL = '^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$'

class CustomValidator(Validator):
    # [Custom input_data Types]
    # http://docs.python-cerberus.org/en/stable/customize.html#custom-input_data-types
    def _validate_type_email(self, value):
        if re.match(REGEX_EMAIL, value):
            return True

schema = {
    'mail_address' : {
        'type' : 'email',
    },
}
data = {
    'mail_address' : 'tanaka_at_test.com',
}

v = CustomValidator(schema)
pprint(v.validate(data))
pprint(v.errors)
```

実行結果

```python
False
{'mail_address': ['must be of email type']}
```

エラーメッセージ 0x24: "must be of {constraint} type", の文言を個別に変更することはできませんが、これで用が足りる場合も多いでしょう。

## 2. バリデーションスキーマにバリデーション用メソッドを指定する (Custom Validator)

スキーマ定義にバリデーション用のメソッド名を指定し、メソッドの実行結果を受け取るイメージです。

```
schema = {
    'mail_address' : {
        'validator': check_mail,   # ←check_mail() メソッド
    },
}
```

これはソースコード見ていただいた方が早いですね。

```python
# -*- coding: utf-8 -*-

import re
from pprint import pprint

from cerberus import Validator

REGEX_EMAIL = '^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$'

# [Custom Validator]
# @see http://docs.python-cerberus.org/en/stable/validation-rules.html#validator]
def check_mail(field, value, error):
    if not re.match(REGEX_EMAIL, value):
        msg = "指定された'%s'は、メールアドレスとして不正です。" % value
        error(field, msg)

schema = {
    'mail_address' : {
        'validator': check_mail,   # ←check_mail() メソッド
    },
}
data = {
    'mail_address' : 'tanaka_at_test.com',
}

v = Validator(schema)
pprint(v.validate(data))
pprint(v.errors)
```

実行結果

```
False
{'mail_address': ["指定された'tanaka_at_test.com'は、メールアドレスとして不正です。"]}
```

毎回これでやるのは大変そうですが、Validatorを拡張しなくていいのは非常に楽です。エラーメッセージも個別に指定できますし。


## 3. バリデーションルールを追加する (Custom Rules)

スキーマに独自のkey, valueを指定したい場合に使います。

```
schema = {
    'mail_address' : {
        'exists_keyword' : 'SUZUKI',
    },
}
```

上記2つと大きく違うのは、「スキーマで指定された値を取得できる」部分です。ここでは 「スキーマで指定されたキーワード(SUZUKI)が含まれていない場合、バリデーションを失敗させる」ようにします。

```python
# -*- coding: utf-8 -*-

import re
from pprint import pprint

from cerberus import Validator

class CustomValidator(Validator):
    # [Custom Rules]
    # http://docs.python-cerberus.org/en/stable/customize.html#custom-rules
    def _validate_exists_keyword(self, exists_keyword, field, value):
        """
        ※以下のdocstringを書かないとwaringが出る
        
        The rule's arguments are validated against this schema: {'type': 'string'}
        """
        if value.find(exists_keyword) < 0:
            self._error(field, "与えられた文字列'%s'の中に、キーワード文字列'%s'が存在しませんでした。" % (value, exists_keyword))


schema = {
    'mail_address' : {
        'exists_keyword' : 'SUZUKI',
    },
}
data = {
    'mail_address' : 'tanaka_at_test.com',
}

v = CustomValidator(schema)
pprint(v.validate(data))
pprint(v.errors)
```

実行結果

```
False
{'mail_address': ["与えられた文字列'tanaka_at_test.com'の中に、キーワード文字列'SUZUKI'が存在しませんでした。"]}
```

docstringを書かないといけない部分がわかりにくいです。今回は {'exists_keyword' : 'SUZUKI'} とスキーマに指定したいので、'SUZUKI'に対応したデータ型 {'type': 'string'} をdocstringに記述しています。[ちなみに`The rule's arguments are...`の文字列は固定です。](https://github.com/nicolaiarocci/cerberus/blob/7408da5be6286ad9a849d859c8fbc31cf616529b/cerberus/validator.py#L1301)

記述しない場合、以下のようなwarningが発生します。
`UserWarning: No validation schema is defined for the arguments of rule 'exists_keyword'`

こちらは複雑な割に、どういう場合に使えばいいのかよくわからなかったのですが、「blacklistをスキーマに渡し、その中にマッチするものがあれば、バリデーションエラーにする」みたいなケースで使えるかもしれません。

なお、スキーマはdict形式でないといけないので、以下のようなカスタムルールを作ってもエラーになります。ご注意ください。

```
# ×NG
schema = {
    'mail_address' : {
        'has_bad_word',           # dict形式になっていない
    },
}

# ◯OK
schema = {
    'mail_address' : {
        'has_bad_word' : True,    # docstringで{'type': 'boolean'} 指定
    },
}
```

# 最後に

めんどくさいバリデーションですが、Cerberusによる「ケロちゃんチェック」で、幸せになりましょう！

あの決定的な敗戦から数十年――