当サイトは、アフィリエイト広告を利用しています
FastApiでREST APIを実装する際に
よく使うレスポンスの実装パターンとその動作について
まとめておく。
FastApiで作成したREST APIではレスポンスを返す際に
いつかの方法がある。
当記事ではよく使われる
の3つのパターンについてまとめる。
またFastApi製のREST API自体の実装方法については当記事では
深くは触れないので知りたい場合は下記記事を参照してください
まずはFastApiでデフォルトでレスポンスを返した場合を実装してみる
FastApiは
を自動で行って後にレスポンスを返却する
from fastapi import FastAPIapp = FastAPI()# ディクショナリuser = {"user_id": "1", "name": "Tujimura", "age": 11}@app.get("/")async def get_user():return user
実行すると
$ curl -i -X GET "http://localhost:8000/" -H "Content-Type: application/json"HTTP/1.1 200 OKdate: Tue, 24 Sep 2024 14:21:02 GMTserver: uvicorncontent-length: 42content-type: application/json{"user_id":"1","name":"Tujimura","age":11}
デフォルトのステータスコードから変えたい場合は
from fastapi import FastAPIapp = FastAPI()# ディクショナリuser = {"user_id": "1", "name": "Tujimura", "age": 11}@app.get("/", status_code=201)async def get_user():return user
にすれば変更できる。
辞書型でなくインスタンスを返してもJSONに変換してくれる
from fastapi import FastAPIfrom typing import Optionalfrom pydantic import BaseModelapp = FastAPI()# pydanticデータモデルclass User(BaseModel):user_id: intname: strage: Optional[int]# Userクラスのインスタンスuser = User(user_id=1, name="Tujimura", age=11)@app.get("/", status_code=201)async def get_user():return user
ただし、レスポンス自動バリデーションをする場合は
下記のように辞書型に変換する必要があるので注意
from fastapi import FastAPIfrom typing import Optionalfrom pydantic import BaseModelapp = FastAPI()# pydanticデータモデルclass User(BaseModel):user_id: intname: strage: Optional[int]# Userクラスのインスタンスuser = User(user_id=1, name="Tujimura", age=11)# response_modelにデータモデルを指定@app.get("/", status_code=201, response_model=User)async def get_user():# 辞書型に変換return user.model_dump()
こうしないとレスポンス自動バリデーションがうまく機能しない。
返却する時はインスタンスから辞書型に変換しておく方が無難だと思う。
デフォルトでエラーが発生した場合の動作を見る
from fastapi import FastAPI, HTTPExceptionfrom typing import Optionalfrom pydantic import BaseModelapp = FastAPI()# pydanticデータモデルclass User(BaseModel):user_id: intname: strage: Optional[int]# Userクラスのインスタンスuser = User(user_id=1, name="Tujimura", age=11)@app.get("/")async def get_user():raise HTTPException(status_code=404, detail="Item not found")
HTTPExceptionを発生させる
実行すると
$ curl -i -X GET "http://localhost:8000/" -H "Content-Type: application/json"HTTP/1.1 404 Not Founddate: Tue, 24 Sep 2024 14:39:23 GMTserver: uvicorncontent-length: 27content-type: application/json{"detail":"Item not found"}
のように自動でHTTPレスポンスを返却してくれる
from fastapi import FastAPI, HTTPExceptionfrom typing import Optionalfrom pydantic import BaseModelapp = FastAPI()# pydanticデータモデルclass User(BaseModel):user_id: intname: strage: Optional[int]# Userクラスのインスタンスuser = User(user_id=1, name="Tujimura", age=11)@app.get("/")async def get_user():raise Exception("Something went wrong")
汎用例外を発生させる
実行すると
$ curl -i -X GET "http://localhost:8000/" -H "Content-Type: application/json"HTTP/1.1 500 Internal Server Errordate: Tue, 24 Sep 2024 14:42:12 GMTserver: uvicorncontent-length: 21content-type: text/plain; charset=utf-8Internal Server Error
同様に自動でHTTPレスポンスを返却してくれる
リクエスト・レスポンスのバリデーションについても
自動でレスポンスを返却してくれる
from fastapi import FastAPIfrom fastapi.responses import JSONResponseimport copyfrom typing import Optionalfrom pydantic import BaseModelapp = FastAPI()# ディクショナリ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}]# pydanticデータモデルclass User(BaseModel):user_id: intname: strage: Optional[int]# 登録# リクエストボディバリデーション@app.post("/")async def post_user(user: User):res_users = copy.deepcopy(users)# Userインスタンスをdictに変換して追加res_users.append(user.model_dump())return JSONResponse(status_code=201, content=res_users)
エンドポイント関数の引数にデータモデルの型ヒントを指定することでリクエストボディの
バリデーションを実行できる
「age」を文字列にしたリクエストを実行すると
$ curl -i -X POST "http://127.0.0.1:8000/" -H "Content-Type: application/json" -d '{"user_id": 5, "name": "nakamura", "age": aa}'HTTP/1.1 422 Unprocessable Entitydate: Tue, 24 Sep 2024 14:47:59 GMTserver: uvicorncontent-length: 125content-type: application/json{"detail":[{"type":"json_invalid","loc":["body",42],"msg":"JSON decode error","input":{},"ctx":{"error":"Expecting value"}}]}
自動でこのようなレスポンスを返却してくれる
from fastapi import FastAPIfrom typing import Optionalfrom pydantic import BaseModelapp = FastAPI()# pydanticデータモデルclass User(BaseModel):user_id: intname: strage: Optional[int]# ディクショナリuser = {"user_id": 1, "name": "Tujimura", "age": 11}@app.get("/", response_model=User)async def get_user():# 文字列に変更user['age']="aaa"# 自動バリデーションreturn user
response_modelにデータモデルを指定することでレスポンスデータのバリデーションを実行できる。
強制的にageを文字列にすることでバリデーションエラーを発生させる
実行すると
$ curl -i -X GET http://localhost:8000/HTTP/1.1 500 Internal Server Errordate: Tue, 24 Sep 2024 14:54:44 GMTserver: uvicorncontent-length: 21content-type: text/plain; charset=utf-8Internal Server Error
レスポンスバリデーションエラーはHTTPレスポンスとしては内部エラー扱いになる。
ログを見ると
raise ResponseValidationError(fastapi.exceptions.ResponseValidationError: 1 validation errors:{'type': 'int_parsing', 'loc': ('response', 'age'), 'msg': 'Input should be a valid integer, unable to parse string as an integer', 'input': 'aaa'}
リクエストバリデーションエラーが発生している
リクエストのパスパラメータやクエリパラメータのバリデーションや
その他のバリデーション方法等は詳しくは下記記事でまとめています。
JSON形式のレスポンスに特化したクラスである「JSONResponse」を
使ってレスポンスを生成し、返却する
JSONResponseは自動で
をしてくれる
ただ注意点として自動シリアライズは
を対象にしか実行されない。
例えばインスタンスの場合は「TypeError: Object of type Users is not JSON serializable」
が発生する。
また自動レスポンスバリデーションも実行されなくなる
JSONResponseでレスポンスを生成
from fastapi import FastAPIfrom typing import Optionalfrom pydantic import BaseModelfrom fastapi.responses import JSONResponseapp = FastAPI()# pydanticデータモデルclass User(BaseModel):user_id: intname: strage: Optional[int]# ディクショナリuser = {"user_id": 1, "name": "Tujimura", "age": 11}@app.get("/")async def get_user():# JSONResponseを使用return JSONResponse(status_code=200, content=user)
実行すると
$ curl -i -X GET http://localhost:8000/HTTP/1.1 200 OKdate: Wed, 25 Sep 2024 13:50:21 GMTserver: uvicorncontent-length: 40content-type: application/json{"user_id":1,"name":"Tujimura","age":11}
自動でcontentがJSON形式に変換される
またrequestsライブラリを使った場合は下記のようになる。
import requestsresponse = requests.get("http://localhost:8000/")print(response.content) # バイト型: b'{"user_id":1,"name":"Tujimura","age":11}'print(response.text) # 文字列型: {"user_id":1,"name":"Tujimura","age":11}
JSONResponseで返却するデータは、
自動でJSONに変換されつつ、バイト型データとしてクライアントに渡されている
データモデルのインスタンスをレスポンスに指定してみる
from fastapi import FastAPIfrom typing import Optionalfrom pydantic import BaseModelfrom fastapi.responses import JSONResponseapp = FastAPI()# pydanticデータモデルclass User(BaseModel):user_id: intname: strage: Optional[int]# Userクラスのインスタンスuser = User(user_id=1, name="Tujimura", age=11)@app.get("/")async def get_user():# JSONResponseを使用return JSONResponse(status_code=200, content=user)
Userクラスのインスタンスをcontentに設定
実行すると
$ curl -i -X GET http://localhost:8000/HTTP/1.1 500 Internal Server Errordate: Wed, 25 Sep 2024 13:55:08 GMTserver: uvicorncontent-length: 21content-type: text/plain; charset=utf-8Internal Server Error
内部エラーとなり、ログを見ると
File "/usr/local/lib/python3.12/json/encoder.py", line 180, in defaultraise TypeError(f'Object of type {o.__class__.__name__} 'TypeError: Object of type User is not JSON serializable
シリアライズが失敗している。
インスタンスを返したい場合は下記のように辞書型に変換する
必要がある
from fastapi import FastAPIfrom typing import Optionalfrom pydantic import BaseModelfrom fastapi.responses import JSONResponseapp = FastAPI()# pydanticデータモデルclass User(BaseModel):user_id: intname: strage: Optional[int]# Userクラスのインスタンスuser = User(user_id=1, name="Tujimura", age=11)@app.get("/")async def get_user():# JSONResponseを使用return JSONResponse(status_code=200, content=user.model_dump())
JSONResponseではレスポンスバリデーションを実行することができない
from fastapi import FastAPIfrom typing import Optionalfrom pydantic import BaseModelfrom fastapi.responses import JSONResponseapp = FastAPI()# pydanticデータモデルclass User(BaseModel):user_id: intname: strage: Optional[int]# ディクショナリuser = {"user_id": 1, "name": "Tujimura", "age": 11}@app.get("/", response_model=User)async def get_user():# バリデーションエラーを発生させるために変更user["age"]="aa"# JSONResponseを使用(レスポンスバリデーション無効)return JSONResponse(status_code=200, content=user)
レスポンスバリデーションエラーが発生するように
ageに文字列を入れて実行すると
$ curl -i -X GET http://localhost:8000/HTTP/1.1 200 OKdate: Wed, 25 Sep 2024 14:01:08 GMTserver: uvicorncontent-length: 42content-type: application/json{"user_id":1,"name":"Tujimura","age":"aa"}
レスポンスバリデーションが動かないのでageが文字列のまま返却される
レスポンスバリデーションしたい場合は手動で行う必要がある
from fastapi import FastAPIfrom typing import Optionalfrom pydantic import BaseModelfrom fastapi.responses import JSONResponseapp = FastAPI()# pydanticデータモデルclass User(BaseModel):user_id: intname: strage: Optional[int]# ディクショナリuser = {"user_id": 1, "name": "Tujimura", "age": 11}@app.get("/", response_model=User)async def get_user():# バリデーションエラーを発生させるために変更user["age"]="aa"# 手動バリデーションvalidated_user = User(**user)# JSONResponseを使用return JSONResponse(status_code=200, content=validated_user.model_dump())
実行すると
$ curl -i -X GET http://localhost:8000/HTTP/1.1 500 Internal Server Errordate: Wed, 25 Sep 2024 14:03:00 GMTserver: uvicorncontent-length: 21content-type: text/plain; charset=utf-8Internal Server Error
内部エラーとなり、ログを見ると
pydantic_core._pydantic_core.ValidationError: 1 validation error for UserageInput should be a valid integer, unable to parse string as an integer [type=int_parsing, input_value='aa', input_type=str]For further information visit https://errors.pydantic.dev/2.9/v/int_parsing
バリデーションエラーが発生している
Responseは、FastAPIにおけるHTTPレスポンスを手動で構築するためのクラス。
FastApiのデフォルトやJSONResponseでは自動で
色々設定してくれたがResponseはすべて手動で設定する感じ。
※レスポンスバリデーションも実行されない
Responseクラスは下記のとおり
class Response:def __init__(self,content: Any = None,status_code: int = 200,headers: Optional[Dict[str, str]] = None,media_type: Optional[str] = None,background: Optional[BackgroundTasks] = None):
from fastapi import FastAPI, Responsefrom typing import Optionalfrom pydantic import BaseModelimport jsonapp = FastAPI()# pydanticデータモデルclass User(BaseModel):user_id: intname: strage: Optional[int]# ディクショナリuser = {"user_id": 1, "name": "Tujimura", "age": 21}@app.get("/")async def get_user():# Responseを使用return Response(status_code=200, content=json.dumps(user), media_type="application/json")
レスポンスクラスから
を引数にして作成したレスポンスを返却している
JSONResponseと同様にレスポンスバリデーションは実行されないので
バリデーションする場合は手動で行う必要がある。
from fastapi import FastAPI, Responsefrom typing import Optionalfrom pydantic import BaseModelapp = FastAPI()# pydanticデータモデルclass User(BaseModel):user_id: intname: strage: Optional[int]# ディクショナリuser = {"user_id": 1, "name": "Tujimura", "age": 11}@app.get("/", response_model=User)async def get_user():# 手動バリデーションvalidated_user = User(**user)# Responseを使用return Response(status_code=200, content=validated_user.model_dump_json(), media_type="application/json")
手動バリデーション成功時はデータモデルのインスタンスになるので
contentに渡す時にインスタンス→JSONに変換して渡す。
FastApiのREST APIでよく使うレスポンス実装パターンをまとめてみた。
FastApiはpydanticをネイティブに統合しているため、リクエスト・レスポンスのバリデーションが
かなり使いやすいが、実装方法によって使えないので注意する。
リクエスト・レスポンスのバリデーションについては下記で詳しくまとめています