当サイトは、アフィリエイト広告を利用しています

【FastApi】REST APIでよく使うレスポンスパターンについて

作成日:2024月09月25日
更新日:2024年09月25日

FastApiでREST APIを実装する際に
よく使うレスポンスの実装パターンとその動作について
まとめておく。

レスポンスについて

FastApiで作成したREST APIではレスポンスを返す際に
いつかの方法がある。

当記事ではよく使われる

  • FastApiのデフォルト
  • JSONResponse
  • Response

の3つのパターンについてまとめる。

またFastApi製のREST API自体の実装方法については当記事では
深くは触れないので知りたい場合は下記記事を参照してください

FastApiのデフォルト

まずはFastApiでデフォルトでレスポンスを返した場合を実装してみる
FastApiは

  • ステータスコードを200にする
  • responseをJSONにシリアライズする
  • コンテンツタイプを application/jsonに設定
  • HTTP例外(404)のエラーハントリング
  • 汎用例外(500)のエラーハントリング
  • バリデーションエラーのエラーハントリング

を自動で行って後にレスポンスを返却する

デフォルト_正常

main.py
from fastapi import FastAPI
app = 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 OK
date: Tue, 24 Sep 2024 14:21:02 GMT
server: uvicorn
content-length: 42
content-type: application/json
{"user_id":"1","name":"Tujimura","age":11}
  • ステータスコードは200OK
  • コンテントタイプはapplication/json
  • データはJSON形式になっている

ステータスコードを変えたい

デフォルトのステータスコードから変えたい場合は

main.py
from fastapi import FastAPI
app = FastAPI()
# ディクショナリ
user = {"user_id": "1", "name": "Tujimura", "age": 11}
@app.get("/", status_code=201)
async def get_user():
return user

にすれば変更できる。

インスタンスを返したい

辞書型でなくインスタンスを返してもJSONに変換してくれる

main.py
from fastapi import FastAPI
from typing import Optional
from pydantic import BaseModel
app = FastAPI()
# pydanticデータモデル
class User(BaseModel):
user_id: int
name: str
age: Optional[int]
# Userクラスのインスタンス
user = User(user_id=1, name="Tujimura", age=11)
@app.get("/", status_code=201)
async def get_user():
return user

ただし、レスポンス自動バリデーションをする場合は
下記のように辞書型に変換する必要があるので注意

main.py
from fastapi import FastAPI
from typing import Optional
from pydantic import BaseModel
app = FastAPI()
# pydanticデータモデル
class User(BaseModel):
user_id: int
name: str
age: 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()

こうしないとレスポンス自動バリデーションがうまく機能しない。
返却する時はインスタンスから辞書型に変換しておく方が無難だと思う。

デフォルト_異常

デフォルトでエラーが発生した場合の動作を見る

HTTP例外(404)のエラーハントリング

main.py
from fastapi import FastAPI, HTTPException
from typing import Optional
from pydantic import BaseModel
app = FastAPI()
# pydanticデータモデル
class User(BaseModel):
user_id: int
name: str
age: 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を発生させる

実行すると

実行_HTTPException
$ curl -i -X GET "http://localhost:8000/" -H "Content-Type: application/json"
HTTP/1.1 404 Not Found
date: Tue, 24 Sep 2024 14:39:23 GMT
server: uvicorn
content-length: 27
content-type: application/json
{"detail":"Item not found"}

のように自動でHTTPレスポンスを返却してくれる

汎用例外(500)のエラーハントリング

main.py
from fastapi import FastAPI, HTTPException
from typing import Optional
from pydantic import BaseModel
app = FastAPI()
# pydanticデータモデル
class User(BaseModel):
user_id: int
name: str
age: Optional[int]
# Userクラスのインスタンス
user = User(user_id=1, name="Tujimura", age=11)
@app.get("/")
async def get_user():
raise Exception("Something went wrong")

汎用例外を発生させる

実行すると

実行_Exception
$ curl -i -X GET "http://localhost:8000/" -H "Content-Type: application/json"
HTTP/1.1 500 Internal Server Error
date: Tue, 24 Sep 2024 14:42:12 GMT
server: uvicorn
content-length: 21
content-type: text/plain; charset=utf-8
Internal Server Error

同様に自動でHTTPレスポンスを返却してくれる

リクエスト・レスポンスのバリデーションエラーについて

リクエスト・レスポンスのバリデーションについても
自動でレスポンスを返却してくれる

リクエストバリデーションエラー

main.py
from fastapi import FastAPI
from fastapi.responses import JSONResponse
import copy
from typing import Optional
from pydantic import BaseModel
app = 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: int
name: str
age: 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 Entity
date: Tue, 24 Sep 2024 14:47:59 GMT
server: uvicorn
content-length: 125
content-type: application/json
{"detail":[{"type":"json_invalid","loc":["body",42],"msg":"JSON decode error","input":{},"ctx":{"error":"Expecting value"}}]}

自動でこのようなレスポンスを返却してくれる

レスポンスバリデーションエラー

main.py
from fastapi import FastAPI
from typing import Optional
from pydantic import BaseModel
app = FastAPI()
# pydanticデータモデル
class User(BaseModel):
user_id: int
name: str
age: 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 Error
date: Tue, 24 Sep 2024 14:54:44 GMT
server: uvicorn
content-length: 21
content-type: text/plain; charset=utf-8
Internal 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'}

リクエストバリデーションエラーが発生している

リクエスト・レスポンスのバリデーションエラーの詳細について

リクエストのパスパラメータやクエリパラメータのバリデーションや
その他のバリデーション方法等は詳しくは下記記事でまとめています。

JSONResponse

JSON形式のレスポンスに特化したクラスである「JSONResponse」を
使ってレスポンスを生成し、返却する

JSONResponseは自動で

  • レスポンスの内容をJSON形式にシリアライズ
  • 適切なヘッダー(Content-Type: application/json)の付与

をしてくれる

ただ注意点として自動シリアライズは

  • 辞書形式(dict)
  • JSON 互換のデータ(リスト、基本的なデータ型など)

を対象にしか実行されない。
例えばインスタンスの場合は「TypeError: Object of type Users is not JSON serializable」
が発生する。

また自動レスポンスバリデーションも実行されなくなる

JSONResponseでレスポンス生成

JSONResponseでレスポンスを生成

main.py
from fastapi import FastAPI
from typing import Optional
from pydantic import BaseModel
from fastapi.responses import JSONResponse
app = FastAPI()
# pydanticデータモデル
class User(BaseModel):
user_id: int
name: str
age: Optional[int]
# ディクショナリ
user = {"user_id": 1, "name": "Tujimura", "age": 11}
@app.get("/")
async def get_user():
# JSONResponseを使用
return JSONResponse(status_code=200, content=user)
  • 引数としてスタータスコードとレスポンスデータを設定する。
  • contentには辞書型を指定する必要がある

実行すると

実行_JSONResponse
$ curl -i -X GET http://localhost:8000/
HTTP/1.1 200 OK
date: Wed, 25 Sep 2024 13:50:21 GMT
server: uvicorn
content-length: 40
content-type: application/json
{"user_id":1,"name":"Tujimura","age":11}

自動でcontentがJSON形式に変換される

インスタンスをレスポンスデータに指定した場合

データモデルのインスタンスをレスポンスに指定してみる

main.py
from fastapi import FastAPI
from typing import Optional
from pydantic import BaseModel
from fastapi.responses import JSONResponse
app = FastAPI()
# pydanticデータモデル
class User(BaseModel):
user_id: int
name: str
age: 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に設定

実行すると

実行_JSONResponse_instance
$ curl -i -X GET http://localhost:8000/
HTTP/1.1 500 Internal Server Error
date: Wed, 25 Sep 2024 13:55:08 GMT
server: uvicorn
content-length: 21
content-type: text/plain; charset=utf-8
Internal Server Error

内部エラーとなり、ログを見ると

ログ
File "/usr/local/lib/python3.12/json/encoder.py", line 180, in default
raise TypeError(f'Object of type {o.__class__.__name__} '
TypeError: Object of type User is not JSON serializable

シリアライズが失敗している。

インスタンスを返したい場合は下記のように辞書型に変換する
必要がある

main.py
from fastapi import FastAPI
from typing import Optional
from pydantic import BaseModel
from fastapi.responses import JSONResponse
app = FastAPI()
# pydanticデータモデル
class User(BaseModel):
user_id: int
name: str
age: 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ではレスポンスバリデーションを実行することができない

main.py
from fastapi import FastAPI
from typing import Optional
from pydantic import BaseModel
from fastapi.responses import JSONResponse
app = FastAPI()
# pydanticデータモデル
class User(BaseModel):
user_id: int
name: str
age: 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 OK
date: Wed, 25 Sep 2024 14:01:08 GMT
server: uvicorn
content-length: 42
content-type: application/json
{"user_id":1,"name":"Tujimura","age":"aa"}

レスポンスバリデーションが動かないのでageが文字列のまま返却される

レスポンスバリデーションしたい場合は手動で行う必要がある

main.py
from fastapi import FastAPI
from typing import Optional
from pydantic import BaseModel
from fastapi.responses import JSONResponse
app = FastAPI()
# pydanticデータモデル
class User(BaseModel):
user_id: int
name: str
age: 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 Error
date: Wed, 25 Sep 2024 14:03:00 GMT
server: uvicorn
content-length: 21
content-type: text/plain; charset=utf-8
Internal Server Error

内部エラーとなり、ログを見ると

bash
pydantic_core._pydantic_core.ValidationError: 1 validation error for User
age
Input 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

Responseは、FastAPIにおけるHTTPレスポンスを手動で構築するためのクラス。

FastApiのデフォルトやJSONResponseでは自動で
色々設定してくれたがResponseはすべて手動で設定する感じ。
※レスポンスバリデーションも実行されない

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
):

Responseクラスでレスポンス作成

main.py
from fastapi import FastAPI, Response
from typing import Optional
from pydantic import BaseModel
import json
app = FastAPI()
# pydanticデータモデル
class User(BaseModel):
user_id: int
name: str
age: 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")

レスポンスクラスから

  • ステータスコードを200
  • レスポンスデータ(辞書)をJSONに変換して設定
  • media_typeをapplication/jsonに設定

を引数にして作成したレスポンスを返却している

レスポンスバリデーションが効かない

JSONResponseと同様にレスポンスバリデーションは実行されないので
バリデーションする場合は手動で行う必要がある。

main.py
from fastapi import FastAPI, Response
from typing import Optional
from pydantic import BaseModel
app = FastAPI()
# pydanticデータモデル
class User(BaseModel):
user_id: int
name: str
age: 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をネイティブに統合しているため、リクエスト・レスポンスのバリデーションが
かなり使いやすいが、実装方法によって使えないので注意する。

リクエスト・レスポンスのバリデーションについては下記で詳しくまとめています

参考

関連記事

新着記事

top