[Python] Validation check using decorator

Python
Tips

(旧ブログから移行しました。)

Python をいじったことはあるけど、初めてちゃんとしたアプリを作る機会ができました。
普段は JavaScript を使用している、Python 修行中の身です。

Web API で飛んでくる Json 形式パラメータのバリデーションチェックメモです。
ちなみにフレームワークとして Flask を選んでいます。導入が簡単で DB は別に用意しているためです。

いろいろみてると、デコレータ関数と jsonschema クラスが使えそうなので使ってみました。

デコレータ関数

元の関数に任意の機能を追加できるという解釈でいます。
@func_name 的な感じでかっこよく使えるのもいいなあと思います。
パフォーマンス計測を各関数に簡単に追加できるな〜なんてイメージしました。
デコレータは引数にもとの関数を持つので、前後に処理を追加したりオーバーライドしたりできます。

簡単に、元の関数の処理前にデコレータでの処理してみます。

from functools import wraps


def hello_decorator(f):
    @wraps(f)
    def wrapper(*args, **kw):
        print('Hello Decorator')
        return f(*args, **kw)
    return wrapper


@hello_decorator
def func():
    print('Hello World')
Hello Decorator
Hello World

いい感じ!

バリデーションチェック

JSON パラメータのチェックは以下の 2 段階で行います。

  1. JSON 形式かどうか
  2. 期待した値が来ているか

順を追ってみていきます。

1. JSON 形式かどうか

import json
from functools import wraps
from werkzeug.exceptions import BadRequest, Forbidden
from flask import request


def validate_json(f):
    @wraps(f)
    def wrapper(*args, **kw):
        try:
            request.json
        except BadRequest as e:
            message = "Please post json format"
            return message, 400
        return f(*args, **kw)
    return wrapper

こんなかんじ。

2. 期待した値が来ているかどうか

import json
from functools import wraps
from jsonschema import validate, ValidationError
from flask import request


def validate_schema(schema_name):
    def decorator(f):
        @wraps(f)
        def wrapper(*args, **kw):
            file_name = "path/to/{}.json".format(os.getcwd(), schema_name)
            with open(file_name) as file:
                schema = json.load(file)
            try:
                validate(request.json, schema)
            except ValidationError as e:
                message = "Request parameter is invalid"
                return message, 400
            return f(*args, **kw)
        return wrapper
    return decorator

こんな感じ。 リクエストパラーメータの JSON Schema を用意しておきます。
各 API にデコレータ引数を当てることで、API に対応したバリデーションチェックをおこないます。
デコレータ関数に引数を設けると、一段階層が深くなります。

validate 関数で一発でチェックできるので最高ですね。あと、言語に依存しない形式でファイルを保持できるのも Good。
Schema はJSONschema.netで作りました。 以下のような JSON オブジェクトが存在する場合、

{
  "userId": "hoge",
  "familyName": "hoshino",
  "age": 25,
  "birthday": "19940219",
  "cost": 1000,
  "address": {
    "prefecture": "pre",
    "city": "city",
    "address1": "1",
    "address2": "2"
  }
}

↑ のようば JSON オブジェクトが存在する場合、

↓ のような Schema を吐き出すことができます。
Required や、細かい Validation はここで設定できます。

{
  "definitions": {},
  "$schema": "http://json-schema.org/draft-07/schema#",
  "$id": "http://example.com/root.json",
  "type": "object",
  "title": "The Root Schema",
  "required": ["userId", "familyName", "age", "birthday", "cost", "address"],
  "properties": {
    "userId": {
      "$id": "#/properties/userId",
      "type": "string",
      "title": "The Userid Schema",
      "default": "",
      "examples": ["hoge"],
      "pattern": "^(.*)$"
    },
    "familyName": {
      "$id": "#/properties/familyName",
      "type": "string",
      "title": "The Familyname Schema",
      "default": "",
      "examples": ["hoshino"],
      "pattern": "^(.*)$"
    },
    "age": {
      "$id": "#/properties/age",
      "type": "integer",
      "title": "The Age Schema",
      "default": 0,
      "examples": [25]
    },
    "birthday": {
      "$id": "#/properties/birthday",
      "type": "string",
      "title": "The Birthday Schema",
      "default": "",
      "examples": ["19940219"],
      "pattern": "^(.*)$"
    },
    "cost": {
      "$id": "#/properties/cost",
      "type": "integer",
      "title": "The Cost Schema",
      "default": 0,
      "examples": [1000]
    },
    "address": {
      "$id": "#/properties/address",
      "type": "object",
      "title": "The Address Schema",
      "required": ["prefecture", "city", "address1", "address2"],
      "properties": {
        "prefecture": {
          "$id": "#/properties/address/properties/prefecture",
          "type": "string",
          "title": "The Prefecture Schema",
          "default": "",
          "examples": ["pre"],
          "pattern": "^(.*)$"
        },
        "city": {
          "$id": "#/properties/address/properties/city",
          "type": "string",
          "title": "The City Schema",
          "default": "",
          "examples": ["city"],
          "pattern": "^(.*)$"
        },
        "address1": {
          "$id": "#/properties/address/properties/address1",
          "type": "string",
          "title": "The Address1 Schema",
          "default": "",
          "examples": ["1"],
          "pattern": "^(.*)$"
        },
        "address2": {
          "$id": "#/properties/address/properties/address2",
          "type": "string",
          "title": "The Address2 Schema",
          "default": "",
          "examples": ["2"],
          "pattern": "^(.*)$"
        }
      }
    }
  }
}

これで準備できました。 呼び出す側はこんな感じでめちゃシンプル。

@validate_json
@validate_schema(schema_name='create_user')
def post(self, project_id):
    # Your function...

API を叩いた時に、パラメータに不備がある場合は 400 エラーを返してくれます。
JSON のチェックはもちろんなのですが、デコレータも便利でスマートなので、積極的に使っていく所存です。

Ryoma HOSHINO
I'm a little developer for people, education, society, world