![見出し画像](https://assets.st-note.com/production/uploads/images/8804029/rectangle_large_type_2_71182fb1329aca2543c91545197717d4.jpg?width=1200)
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にサンプルのソースコードを上げました。ご参考になれば幸いです。