見出し画像

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

この記事は以下URLの翻訳記事です。

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
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(),
])]
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)
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にサンプルのソースコードを上げました。ご参考になれば幸いです。


この記事が気に入ったらサポートをしてみませんか?