[Python] Voluptuousを用いたJSONのvalidation

PythonでVoluptuousライブラリを使ってJSONの値の検証(validation)を実施する方法。

# Voluptuousとは
PythonのJSON等のvalidationライブラリ。

公式リポジトリ： https://github.com/alecthomas/voluptuous

> Voluptuous, despite the name, is a Python data validation library. It is primarily intended for validating data coming into Python as JSON, YAML, etc.

リポジトリ内の説明でも触れているが、すごい名前だ。

```commnet:voluptuous
1 肉欲にふける，酒色におぼれる，官能的な.
2 肉感的な，色っぽい，あだっぽい，セクシーな.
3〈感触など〉心地よい.
```

# install
## Python 2.6以上
pipでインストールできる。

```console
# pip install voluptuous
```

## Python 2.5
公式では非サポートなものの [Python2.5でVoluptuousをインストールする](http://qiita.com/hombashi/items/8f08e1221c4213e14460) でインストールと本記事レベルの動作は可。

# Sample program
動作を示すサンプルプログラム。
get_schema 内のSchema宣言が核となるところ。
これがVoluptuousでのJSONスキーマの定義となる。

```python
from __future__ import with_statement
import re
import sys
import json  # Python 2.5ではsimplejson
import six
from voluptuous import Schema, Required, Any, Range, Invalid, ALLOW_EXTRA


def validate_io_size(v):
	if re.search("^[0-9]+[bkm]", v) is None:
		raise Invalid("not a valid value (%s)" % str(v))


def get_schema():
	schema = Schema({
		"comment"					: six.text_type,
		Required("operation")		: Any("read", "write"),
		"thread"					: Range(min=1),
		Required("io_size")			: validate_io_size,
		Required("access_percentage")	: Range(min=1, max=100),
	}, extra=ALLOW_EXTRA)
	
	return schema


def main():
	schema	= get_schema()
	
	with open(sys.argv[1], "r") as fp:
		dict_sample	= json.load(fp)
	
	schema(dict_sample)
	six.print_(dict_sample)
	
	return 0


if __name__ == '__main__':
	sys.exit(main())
```


# 詳細
## Voluptuous用語
キーワード|意味
---|---
Required|必須パラメータ。これをつけていないパラメータは省いてもエラーにならない。
Any|宣言中のいずれかの値ならOK。
Range|min以上、max以下ならOK。min or maxのみ指定することもできる。
ALLOW_EXTRA|スキーマに定義が無いパラメータの存在を許す。

## validationの方法
以下のいずれか

1. 取り得る値の直接指定。Any、Range、等で幅を持たせることもできる。
2. クラス(int, float等の他、six.text_typeといった指定も可)
 - クラスが合致していれば値は何でもOK
3. validation関数を指定する
 - 前述のvalidate_io_size関数がこれ
 - validation関数は条件を満たさない場合はInvalid例外を投げる


# 動作例
## 環境
- Python 2.5/2.7
- voluptuous 0.8.8

## 正常動作例
```json:valid1.json
{
	"comment"			: "comment",
	"operation"			: "write",
	"thread"			: 8,
	"io_size"			: "8k",
	"access_percentage"	: 100
}
```

```console
# python voluptuous_sample.py valid1.json
{u'comment': u'comment', u'operation': u'write', u'access_percentage': 100, u'thread': 8, u'io_size': u'8k'}
#
```

## 正常動作例(必須パラメータ省略)
```json:valid2.json
{
	"operation"			: "write",
	"io_size"			: "8k",
	"access_percentage"	: 100
}
```

```console
# python voluptuous_sample.py valid2.json
{u'operation': u'write', u'access_percentage': 100, u'io_size': u'8k'}
```

必須パラメータ意外は無くてもエラーにならない。

## エラー動作例
```json:invalid.json
{
	"comment"			: "comment",
	"operation"			: "<invalid value>",
	"thread"			: 8,
	"io_size"			: "8k",
	"access_percentage"	: 100
}
```

```pytb
# python voluptuous_sample.py invalid.json
Traceback (most recent call last):
  File "voluptuous_sample.py", line 36, in <module>
    sys.exit(main())
  File "voluptuous_sample.py", line 29, in main
    schema(dict_sample)
  File "/usr/local/lib/python2.7/dist-packages/voluptuous.py", line 337, in __call__
    return self._compiled([], data)
  File "/usr/local/lib/python2.7/dist-packages/voluptuous.py", line 635, in validate_dict
    return base_validate(path, iteritems(data), out)
  File "/usr/local/lib/python2.7/dist-packages/voluptuous.py", line 471, in validate_mapping
    raise MultipleInvalid(errors)
voluptuous.MultipleInvalid: not a valid value for dictionary value @ data[u'operation']
#
```

スキーマに合致しないパラメータがあるとvoluptuous.MultipleInvalidが送出され、
パラメータ（例ではoperation)の値が正しくないよ、というメッセージが出る。

## エラー動作例(validation関数指定時)
```json:invalid2.json
{
	"comment"			: "comment",
	"operation"			: "write",
	"thread"			: 8,
	"io_size"			: "a8k",
	"access_percentage"	: 100
}
```

```pytb
# python voluptuous_sample.py invalid2.json
Traceback (most recent call last):
  File "voluptuous_sample.py", line 38, in <module>
    sys.exit(main())
  File "voluptuous_sample.py", line 31, in main
    schema(dict_sample)
  File "/usr/local/lib/python2.7/dist-packages/voluptuous.py", line 337, in __call__
    return self._compiled([], data)
  File "/usr/local/lib/python2.7/dist-packages/voluptuous.py", line 635, in validate_dict
    return base_validate(path, iteritems(data), out)
  File "/usr/local/lib/python2.7/dist-packages/voluptuous.py", line 471, in validate_mapping
    raise MultipleInvalid(errors)
voluptuous.MultipleInvalid: not a valid value (a8k) for dictionary value @ data[u'io_size']
```

validation関数を指定した場合もこのように。