当サイトは、アフィリエイト広告を利用しています
PythonのFlask製のREST APIでリクエストとレスポンスのバリデーションを
行う方法をまとめる。
Flask自体には組み込みでバリデーション機能ないので
外部ライブラリの「pydantic」を使う。
pydanticは「2.8.2」を使う。
※v2.xから書き方の変更が割とある。
pydanticは型ヒントを使用してデータのバリデーションを行うライブラリ。
pydanticは
を行うことができる。
サンプルプログラムで試してみる。
pydanticのBaseModelクラスを継承した
データモデルを定義する。
from pydantic import BaseModelclass User(BaseModel):user_id: intname: strage: int
データモデルは
に使用する。
データモデルの型ヒントを使用してバリデーションを行う。
from pydantic import BaseModel, ValidationErrorclass User(BaseModel):user_id: intname: strage: inttry:# `user_id`を指定せずにインスタンスを作成する場合、バリデーションエラーが発生するuser = User(name="Alice", age=25)print(user)except ValidationError as e:print(e.json())# 実行結果# [{"type":"missing","loc":["user_id"],"msg":"Field required","input":{"name":"Alice","age":25},"url":"https://errors.pydantic.dev/2.8/v/missing"}]
user_idがないためバリデーションエラーとなる。
バリデーションがすべて成功した場合、Pydanticは渡されたデータを使ってUserクラスのインスタンス(オブジェクト)
を作成する。
このインスタンスは、各フィールドがバリデート済みのデータを持つPythonのデータクラスとして扱うことができる。
from pydantic import BaseModel, ValidationErrorclass User(BaseModel):user_id: intname: strage: inttry:user = User(user_id=2, name="Alice", age=25)print(user)# インスタンスが`User`のものであることを確認if isinstance(user, User):print("`user`はUserのインスタンスです")else:print("`user`はUserのインスタンスではありません")except ValidationError as e:print(e.json())# 実行結果# user_id=2 name='Alice' age=25# `user`はUserのインスタンスです
バリデーションとデータ変換(デシリアライズ)は同時に行われる。
Userクラスのインスタンスからシリアライズする。
from pydantic import BaseModelimport json # 標準ライブラリのjsonモジュールをインポートclass User(BaseModel):user_id: intname: strage: intuser = User(user_id=1, name="Charlie", age=40)# 辞書形式に変換(model_dump()を使用)user_dict = user.model_dump()print(user_dict)# JSON形式に変換(json.dumps()を使用)user_json = json.dumps(user_dict)print(user_json)# 実行結果# {'user_id': 1, 'name': 'Charlie', 'age': 40}# {"user_id": 1, "name": "Charlie", "age": 40}
REST APIの場合はレスポンスはJSONにしてから返却するので
その時に使用する。
FlaskのREST APIでpydanticを使って
の機能を実装してみる。
開発環境としては、dockerを使ったコンテナ環境をつくり
動作を確認するので環境条件は下記のようにする
プロジェクトの構成は下記にする
.|-- .env|-- .vscode| |-- launch.json| `-- tasks.json|-- Dockerfile|-- api| |-- __init__.py| |-- api.py| |-- schemas.py| `-- wsgi.py|-- docker-compose.yml|-- requirements.txt`-- vscode_ex_install.sh
flaskのREST APIを
する方法については下記記事で紹介しているので割愛する
上記記事で作ったプロジェクトに対して
をしているので他のサーバー起動やデバッグはそのまま流用できる。
バリデーションを行うためのデータモデルを定義する
from pydantic import BaseModel, Field, field_validatorfrom typing import Optionalclass Users(BaseModel):user_id: strname: str = Field(..., min_length=1, max_length=50)age: Optional[int] = Field(None, ge=0, le=120)@field_validator('name')def name_must_be_capitalized(cls, v):if not v.istitle():raise ValueError('名前は大文字で始める必要があります')return v
型ヒントでバリデーションを行うことができる
文字列型の必須フィールド
文字列型の必須フィールド。
またFieldを使って下記の制約をしている
整数型のフィードで必須ではない。 またFieldを使って下記の制約をしている
nameに対してカスタムバリデーションを設定。
nameフィールドの値がタイトルケース(つまり、各単語が大文字で始まる)でない場合に
バリデーションエラーを発生させる。
api.pyを変更してREST APIの
を行う。
api.py全体のソースを載せる。
from flask import Flask, request, jsonify,make_responseimport copyfrom pydantic import ValidationErrorfrom api.schemas import Users# ディクショナリusers = [{"user_id": "1", "name": "Tujimura", "age": 11},{"user_id": "2", "name": "mori", "age": 20},{"user_id": "3", "name": "shimada", "age": 50},{"user_id": "4", "name": "kyogoku", "age": 70}]# 関数ベースのルーティング関数def routers(api):# バリデーションエラーハンドラー@api.errorhandler(ValidationError)def handle_validation_error(error):print(error.json())response = {"status_code": "422","detail": str(error.errors())}# カスタム属性 `context` でリクエストかレスポンスのエラーかを区別if hasattr(error, 'context') and error.context == 'request':response["error_message"] = "Request validation error"return make_response(jsonify(response), 422)elif hasattr(error, 'context') and error.context == 'response':response["error_message"] = "Response validation error"return make_response(jsonify(response), 422)# user_idを指定して取得@api.route('/<user_id>', methods=['GET'])def get_user_by_id(user_id):user = next((user for user in users if user['user_id'] == user_id), None)# レスポンスバリデーションtry:# バリデーション実行validated_data = Users(**user)except ValidationError as e:e.context = 'response'raise eif user:return make_response(jsonify(validated_data.model_dump()),200)return make_response(jsonify({"error": "User not found"}), 404)# 登録@api.route('/', methods=['POST'])def post_user():# リクエストバリデーションtry:# リクエストボディをdictに変換request_data = request.get_json()# バリデーション実行validated_data = Users(**request_data)# validated_data = Users.model_validate(request_data)except ValidationError as e:e.context = 'request'raise eres_users = copy.deepcopy(users)res_users.append(validated_data.model_dump())return make_response(jsonify(res_users), 201)# flaskインスタンス作成def create_app():# 作成api = Flask(__name__)# 日本語文字化け対応api.json.ensure_ascii = False# 関数ベースのルーティング設定routers(api)return api
細かく解説する。
# バリデーションエラーハンドラー@api.errorhandler(ValidationError)def handle_validation_error(error):print(error.json())response = {"status_code": "422","detail": str(error.errors())}# カスタム属性 `context` でリクエストかレスポンスのエラーかを区別if hasattr(error, 'context') and error.context == 'request':response["error_message"] = "Request validation error"return make_response(jsonify(response), 422)elif hasattr(error, 'context') and error.context == 'response':response["error_message"] = "Response validation error"return make_response(jsonify(response), 422)
handle_validation_errorをエラーハンドラー関数として登録することで
アプリケーション内で発生したValidationErrorはすべてこの関数で処理させる。
※try~exceptで捕まえた場合を除く。
handle_validation_errorは例外オブジェクトを引数として受け取り
コンテキストの値でリクエストとレスポンスのどちらのバリデーションエラーかを
判定して処理する。
# 登録@api.route('/', methods=['POST'])def post_user():# リクエストバリデーションtry:# リクエストボディをdictに変換request_data = request.get_json()# バリデーション実行validated_data = Users(**request_data)# validated_data = Users.model_validate(request_data)except ValidationError as e:e.context = 'request'raise eres_users = copy.deepcopy(users)res_users.append(validated_data.model_dump())return make_response(jsonify(res_users), 201)
「validated_data = Users(**request_data)」でバリデーションを実行.
schemas.pyで定義したPydanticのBaseModelを継承したUserクラスは、受け取ったキーワード引数をもとに
自動的にバリデーションと型変換を行い、インスタンスを作成する。
ValidationErrorが発生した場合は例外オブジェクトにコンテキストとして
「request」を追加して、再スローする。
ValidationErrorだけではリクエストのバリデーションエラーか、レスポンスのバリデーションエラー
かを判定できないので、コンテキスト情報を追加して再スローしている。
※再スロー後はハンドラー関数で処理される
コメントアウトしている「Users.model_validate」を使っても
バリデーションを実行できるが、基本的には「Users(**request_data)」で良いと思う。
# user_idを指定して取得@api.route('/<user_id>', methods=['GET'])def get_user_by_id(user_id):user = next((user for user in users if user['user_id'] == user_id), None)# レスポンスバリデーションtry:# バリデーション実行validated_data = Users(**user)except ValidationError as e:e.context = 'response'raise eif user:return make_response(jsonify(validated_data.model_dump()),200)return make_response(jsonify({"error": "User not found"}), 404)
取得した値に対してバリデーションを実行する。
基本的にはリクエストのバリデーションと同じ。
ValidationError発生時はコンテキスト情報として、「response」を追加して
再フローする。
※再スロー後はハンドラー関数で処理される
実装したバリデーションが機能するか確認する
サーバーの起動は下記で行う
flask run --host=0.0.0.0
コンテナ上で動かす前提。
登録時のリクエストバリデーションが作動することを確認する ホストからcurlで試す。
$ curl -X POST http://localhost:5000 -H "Content-Type: application/json" -d '{ "user_id": "1", "name": "tujimura", "age": 11}'{"detail": "[{'type': 'value_error', 'loc': ('name',), 'msg': 'Value error, 名前は大文字で始める必要があります', 'input': 'tujimura', 'ctx': {'error': ValueError('名前は大文字で始める必要があります')}, 'url': 'https://errors.pydantic.dev/2.8/v/value_error'}]","error_message": "Request validation error","status_code": "422"}
カスタムバリデーションが機能している。
$ curl -X GET http://localhost:5000/2{"detail": "[{'type': 'value_error', 'loc': ('name',), 'msg': 'Value error, 名前は大文字で始める必要があります', 'input': 'mori', 'ctx': {'error': ValueError('名前は大文字で始める必要があります')}, 'url': 'https://errors.pydantic.dev/2.8/v/value_error'}]","error_message": "Response validation error","status_code": "422"}
2のデータは
{"user_id": "2", "name": "mori", "age": 20},
であるため、カスタムバリデーションでエラーになる。
1の場合は
{"user_id": "1", "name": "Tujimura", "age": 11},
なので
$ curl -X GET http://localhost:5000/1{"age": 11,"name": "Tujimura","user_id": "1"}
バリデーションエラーは発生しない
上記ではバリデーションを行うためにtry~exceptで囲んでいたが
エンドポイントが増えてくると同じことを繰り返し書く必要があり
無駄になるため、バリデーション関数の関数を作成して効率的にバリデーションを行ってみる
api.pyを変更する
from flask import Flask, request, jsonify,make_responseimport copyfrom pydantic import ValidationError, BaseModelfrom typing import Optional, Typefrom api.schemas import Users# ディクショナリusers = [{"user_id": "1", "name": "Tujimura", "age": 11},{"user_id": "2", "name": "mori", "age": 20},{"user_id": "3", "name": "shimada", "age": 50},{"user_id": "4", "name": "kyogoku", "age": 70}]def validation_check(data: dict, model: Type[BaseModel],target: str):# レスポンスのバリデーションチェックtry:validated_data = model(**data) # 引数で渡されたモデルを使用してバリデーションreturn validated_dataexcept ValidationError as e:if target == 'request':e.context = 'request' # エラーオブジェクトにコンテキスト情報を追加else:e.context = 'response' # エラーオブジェクトにコンテキスト情報を追加raise e # eを再スロー# 関数ベースのルーティング関数def routers(api):# バリデーションエラーハンドラー@api.errorhandler(ValidationError)def handle_validation_error(error):print(error.json())response = {"status_code": "422","detail": str(error.errors())}# カスタム属性 `context` でリクエストかレスポンスのエラーかを区別if hasattr(error, 'context') and error.context == 'request':response["error_message"] = "Request validation error"return make_response(jsonify(response), 422)elif hasattr(error, 'context') and error.context == 'response':response["error_message"] = "Response validation error"return make_response(jsonify(response), 422)# user_idを指定して取得@api.route('/<user_id>', methods=['GET'])def get_user_by_id(user_id):user = next((user for user in users if user['user_id'] == user_id), None)# レスポンスバリデーションvalidated_data = validation_check(data=user, model=Users, target='response')if user:return make_response(jsonify(validated_data.model_dump()),200)return make_response(jsonify({"error": "User not found"}), 404)# 登録@api.route('/', methods=['POST'])def post_user():# リクエストバリデーションrequest_data = request.get_json()validated_data = validation_check(data=request_data, model=Users, target='request')res_users = copy.deepcopy(users)res_users.append(validated_data.model_dump())return make_response(jsonify(res_users), 201)# flaskインスタンス作成def create_app():# 作成api = Flask(__name__)# 日本語文字化け対応api.json.ensure_ascii = False# 関数ベースのルーティング設定routers(api)return api
validation_check関数内でバリデーションを行うようにする。
flaskでpydanticを使ってリクエストとレスポンスのバリデーション機能を
実装してみた。
pydantic自体もシンプルで使いやすいのでFlaskでバリデーション機能を
実装する際の選択肢としてはかなり有力だと思う。
またFlaskのREST APIでバリデーションチェックにpydanticを使用している
実例として下記の記事が参考になるかと思います