jsonスキーマの色々な機能を使用する

# はじめに
[pythonでjsonスキーマを使用する](https://qiita.com/mink0212/items/8564d2ed5cf778d539c5)では、jsonスキーマの簡単な使用方法をまとめました。
jsonスキーマには、さらに色々な機能がありますが、あまり知られていないためまとめようと思います。そのため、ここにまとめてあるのはjsonスキーマでチェックできる型や上限など良く知られているものは省略しています。



# 環境
 - python:3.6.5
 - jsonschema:3.0.1
  

# 値チェックの色々
## 複数値内で値が合致しているものがあるかチェックする
入力値があらかじめ決まっている場合に```enum```に設定してあげることで設定した値が入力されたらOK、入力された値が設定外ならエラーになります。


``` json:schema.json
{
    "$schema": "http://json-schema.org/draft-07/schema#",
    "title": "PyJsonValidate",
    "description": "sample json for json validate",
    "type": "object",
    "properties" :{
        "kind": {
            "type": "string",
            "enum": [
                "dog",
                "cat"
            ]
        }
    }
}
```


サンプル用のpython

``` python:json_schema_main.py
import json
from jsonschema import validate, ValidationError

with open('schema.json') as file_obj:
    json_schema = json.load(file_obj)

item = {
    "kind": "mouse",
}

try:
    validate(item, json_schema)
except ValidationError as e:
    print(e.message)

print('END')
```

結果

```
'mouse' is not one of ['dog', 'cat']
END
```

jsonスキーマでkindキーの値はdog、catだと指定しているため、mouseを入力するとエラーして、mouseはdogでもcatでもないという旨のメッセージが表示されました。






## 同一キーで複数のチェックパターンを設定する
あまり仕様的に褒められたものではないですが、同じキーを再利用して全く別の値を入力することがあります。その際に、```anyOf```を使用すると同じキーでありながら別パターンのチェックをすることができます。


``` json:schema.json
{
    "$schema": "http://json-schema.org/draft-07/schema#",
    "title": "PyJsonValidate",
    "description": "sample json for json validate",
    "type": "object",
    "properties": {
        "kind": {
            "anyOf": [
                {"type": "string"},
                {"type": "integer"}
            ]
        }
    }
}
```

サンプル用のpythonは上と同じです。
itemの値だけ変更しています。


結果

```
0.01 is not of type 'string'
END
```

jsonスキーマでkindキーの値は文字列と整数と指定しているため、0.01を入力するとエラーしました。また、当然ですが文字列と整数を入力した場合はエラーせずに終了しました。
今回は型の指定のみしましたが{}内に上限値など他の設定を入れるとその設定もそれぞれに有効になります。



## 複数パターンのキーをチェックする
anyOfの応用です。これもあまり仕様的に褒められたものではないですが、場合によって入力されるキーのパターンが複数ある場合にも```anyOf```を使うことでチェックできます。


``` json:schema.json
{
    "$schema": "http://json-schema.org/draft-07/schema#",
    "title": "PyJsonValidate",
    "description": "sample json for json validate",
    "type": "object",
    "anyOf":[
            {"additionalProperties": false, "properties":{"cat": {"type": "string"}}},
            {"additionalProperties": false, "properties":{"dog": {"type": "string"}}}
    ]
}
```

サンプル用のpythonは上と同じです。
itemの値だけ変更しています。


結果

```
Additional properties are not allowed ('mouse' was unexpected)
END
```

jsonスキーマでキーがcatまたはdogと指定しているため、itemのキーをmouseにして入力するとエラーしました。また、当然ですがdogとcatのキーを入力した場合はエラーせずに終了しました。


## あるパターンのキーをチェックする
キーとキーの間の整合性を取るために2つのうちどちらかのキーのみ入力してほしい場合があります。その場合には```oneOf```を使うことでチェックできます。


``` json:schema.json
{
    "$schema": "http://json-schema.org/draft-07/schema#",
    "title": "PyJsonValidate",
    "description": "sample json for json validate",
    "type": "object",
    "oneOf":[
            {"additionalProperties": false, "properties":{"cat": {"type": "string"}}},
            {"additionalProperties": false, "properties":{"dog": {"type": "string"}}}
    ]
}
```

サンプル用のpythonは上と同じです。
itemの値だけ変更しています。

``` python:json_schema_main.py
item = {
    "dog": "pochi",
    "cat": "tama"
}
```

結果

```
Additional properties are not allowed ('dog' was unexpected)
END
```

正常パターン

``` python:json_schema_main.py
item = {
    "dog": "pochi"
}
```

結果

```
END
```

jsonスキーマでキーがcatとdogのパターンを禁止しているため、itemにdogとcatを入力するとエラーしました。また、当然ですがcatのキーだけを入力した場合はエラーせずに終了しました。anyOfと同様にこれも様々な設定をできます。


## 指定した以外の値を受け取る
ある値以外を欲しいという場合に```allOf```と```not```を使用してチェックできます。


``` json:schema.json
{
    "$schema": "http://json-schema.org/draft-07/schema#",
    "title": "PyJsonValidate",
    "description": "sample json for json validate",
    "type": "object",
    "properties":{
        "dog": {
            "allOf": [
                {"type": "string"},
                {"not": {"enum": ["pochi", "tama"]}}
            ]
        }
    }
}
```

サンプル用のpythonは上と同じです。
itemの値だけ変更しています。

``` python:json_schema_main.py
item = {
    "dog": "pochi"
}
```

結果

```
{'enum': ['pochi', 'tama']} is not allowed for 'pochi'
END
```

正常パターン

``` python:json_schema_main.py
item = {
    "dog": "tibi"
}
```

結果

```
END
```

enumでpochiとtamaだけの設定をnotで反転させ、pochiとtamaは受け付けないようにしました。また、allOfを使用して文字列と値の指定両方を満たすようにチェックしています。特にallOfを使用する必要はありませんが丁度良かったので使いました。


# jsonスキーマの表記の色々
## 設定をリンクで指定する
設定が複雑になっていくと設定だけ別ファイルや別ブロックにまとめたい場合があります。その場合は、```$ref```に指定してあげることでリンクすることができます。


``` json:schema.json
{
    "$schema": "http://json-schema.org/draft-07/schema#",
    "title": "PyJsonValidate",
    "description": "sample json for json validate",
    "type": "object",
    "$ref": "#/definitions/dog",
    "definitions": {
        "dog": {
            "properties" :{
                "kind": {
                    "type": "string",
                    "enum": ["dog"]
                }
            }
        }
    }
}
```

``` python
item = {
    "kind": "cat",
}
```

結果

```
'cat' is not one of ['dog']
END
```

refで指定したdefinitions/dogの設定が反映されました。
上記例はあまり見やすくなった気がしないですが、anyOfやoneOfなど複雑になっていくとこの設定があるかないかで大きく見やすさが変わります。


## 設定を別ファイルに分ける
次は別ファイルに分けていきます。json側では```$ref```にファイル名を指定してあげて、python側の設定で別ファイルのリンクを設定してあげます。
python側の設定はjsonschemaのライブラリのRefResolverクラスに設定ファイルがあるディレクトリと元のjsonスキーマを渡してあげてvalidate時に指定してあげる必要があります。


``` json:schema.json
{
    "$schema": "http://json-schema.org/draft-07/schema#",
    "title": "PyJsonValidate",
    "description": "sample json for json validate",
    "type": "object",
    "$ref": "dog.json"
}
```

``` json:dog.json
{
    "properties" :{
        "kind": {
            "type": "string",
            "enum": ["dog"]
        }
    }
}
```

``` python:json_schema_main.py
import os
import json
from jsonschema import validate, ValidationError, RefResolver

with open('schema.json') as file_obj:
    json_schema = json.load(file_obj)


item = {
    "kind": "cat",
}

try:
    schema_path = 'file:///{0}/'.format((os.getcwd()).replace("\\", "/"))
    ref = RefResolver(schema_path, json_schema)
    validate(item, json_schema, resolver=ref)
except ValidationError as e:
    print(e.message)

print('END')
```

結果

```
'cat' is not one of ['dog']
END
```

jsonschemaライブラリの使い方が少しネックですが、jsonファイルはとても分かりやすくなりました。今回は設定まるまる別ファイルにしましたがやろうと思えばどんな設定も分けられるため、ルールを決めてファイル分けすると見やすくなるのではないかと思います。


## おわりに
jsonスキーマの色々な使い方についてまとめました。おそらく、頭を使えばほとんどの入力値チェックがjsonスキーマで可能なほど汎用性のあるものでした。
ただ、ここまでやってしまうと人が見てわかりやすいjsonという利点をつぶしてしまいそうなので個人的にはよっぽどの理由がない限りは単純なjsonを使用しています。
開発の時に他の人の技術レベルを考えて開発していますが、技術レベルと開発のレベルはトレードオフの関係なので、どのレベルに抑えるのかがすごい悩みます。