ShimakazeSoft Tech

Python好きの新卒WEBエンジニアが技術記事を執筆するブログ。主にWEB系や機械学習系のことを掲載。

Falcon リクエストのバリデーションチェック, シリアライザミドルウェア(marshmallow) (翻訳)

falconロゴ
Falcon

この記事は以下URLの翻訳記事です。
Falcon framework - request data validation, serializer middleware (marshmallow) - Przemysław `eshlox` Kołodziejczyk


Falconにはリクエストの内容をバリデーション(検証)する仕組みは備えていませんが、幸いにもその機能を追加することは簡単です。

marshmallowを使ってリクエストに付随するデータを検証します。これにより、スキーマを作成して、JSONデータが正しいかどうかを検証します。

ミドルウェアの使い方がわからない場合は、こちらでFalconミドルウェアのドキュメントが読めます。


それではまずカスタムのHTTPErrorクラスを作ることから始めましょう。
なぜ最初にそれらをする必要があるのか。
それは、送信されてきたデーターが何が問題なのかをメッセージで返したい場合、以下の様にして返さなければならないからです。

{
    "title": "422 Unprocessable Entity",
    "errors": {
        "date_start": ["Missing data for required field."]
    }
}

上記データーのtitleフィールドはHTTPエラーの説明ですが、エラーはmasrhmallowから送られてきます。以下は、marshmallowがどのようにしてデータをバリデーション(検証)するかの例です。

from marshmallow import fields, Schema, ValidationError


class UserSchema(Schema):
    name = fields.Str(required=True)


try:
    UserSchema(strict=True).load({})
except ValidationError as err:
    print(err.messages)


上記のとおり、単一のnameフィールドを持つUserSchemaクラスを作成しています。nameフィールドは必須項目です。
nameフィールドを渡さずにUserSchemaオブジェクトを作成しようとした場合、ValidationErrorが発生するはずです。

上記スクリプトの出力結果(バリデーションエラーが発生した時)

{'name': ['Missing data for required field.']}

上記の情報をユーザーに返したいとします。
デフォルトのHTTPErrorでは、文字列部分を説明文としてのみ指定できますが、今回は辞書型(dict)で返したいと考えています。
そのため、デフォルトのHTTPErrorクラスの内容を少し変更します。

import falcon


class HTTPError(falcon.HTTPError):
    """
    HTTPError that stores a dictionary of validation error messages.
    バリデーションエラーメッセージの辞書を格納するHTTPErrorクラス
    """

    def __init__(self, status, errors=None, *args, **kwargs):
        self.errors = errors
        super().__init__(status, *args, **kwargs)

    def to_dict(self, *args, **kwargs):
        """
        Override `falcon.HTTPError` to include error messages in responses.

        レスポンス内にエラーメッセージに格納するために`falcon.HTTPError`をオーバーライドしてください。
        """

        ret = super().to_dict(*args, **kwargs)

        if self.errors is not None:
            ret['errors'] = self.errors

        return ret

上記の新しいHTTPErrorをミドルウェア内で使用することで、バリデーションエラーを返すことができます。それではミドルウェア部分の処理を書きましょう。

import falcon.status_codes as status

from marshmallow import ValidationError

from core.errors import HTTPError  # it's our new HTTPError


class SerializerMiddleware:

    def process_resource(self, req, resp, resource, params):
        req_data = req.context.get('request') or req.params

        try:
            serializer = resource.serializers[req.method.lower()]
        except (AttributeError, IndexError, KeyError):
            return
        else:
            try:
                req.context['serializer'] = serializer().load(
                    data=req_data
                ).data
            except ValidationError as err:
                raise HTTPError(status=status.HTTP_422, errors=err.messages)

このミドルウェアは、リクエストされたクエリ文字列とボディからデータをバリデーション(検証)します。

デフォルトreq.context内にリクエストはありませんが、ここでのリクエストではありません。
別のミドルウェアを使用してユーザーからJSONデータを読み込んでそこに設定しましたが、このミドルウェアではJSONデーターを読み込むことができます。
また、全てのHTTPメソッドに対して個別のスキーマ(バリデータ)を設定することもできます。
最後に、シリアライザーデータをコンテキストに設定すると、APIエンドポイントのデータを読み取ることができます。
データが正しくない場合、APIはmarshmallowから返されたバリデーションメッセージでHTTP 422エラーを返します。それでは簡単な例を書きましょう。

まず最初に、Falconアプリケーションにミドルウェアを登録します。

import falcon

from core.middleware.serializers import SerializerMiddleware


app = falcon.API(middleware=[
    SerializerMiddleware(),
])

次に、単純なmarshmallowスキーマを作成します。

from marshmallow import fields, Schema

class BookPostSchema(Schema):
    class Meta:
        strict = True

    title = fields.Str(required=True)

class BookDeleteSchema(Schema):
    class Meta:
        strict = True

    book_id = fields.Integer(required=True)

単純なAPIエンドポイント

from book.serializers import BookDeleteSchema, BookPostSchema


class BookAPI:
    serializers = {
        'post': BookPostSchema,
        'delete': BookDeleteSchema
    }

    def on_post(self, req, resp):
        serializer = req.context['serializer']
        # req.context['serializer'] contains data sent by user
        # for example: print(serializer['title'])

    def on_delete(self, req, resp):
        serializer = req.context['serializer']
        print(serializer['book_id'])

    def on_put(self, req, resp):
        # no schema for delete method == no data validation

上記の通り、これでエンドポイントの全てのHTTPメソッドに異なるスキーマを割り当てることができます。データーが正しい場合、req.context['serializer']にアクセスが可能です。そうでない場合、APIはHTTPエラーを返します。




一部、誤訳も含めているかもしれないためご指摘いただければ修正します。

GitHubにサンプルのソースコードを上げました。ご参考になれば幸いです。
The world’s leading software development platform · GitHubgithub.com