概要(レスポンスモデルは「返していい形の“ふるい”」)
レスポンスモデルは、
「この API は、こういう形・こういう型の JSON を“返す”はずだ」
という“約束(スキーマ)”をコードで表現したものです。
FastAPI では、Pydantic モデルを response_model=... で指定することで、
- 返す JSON の“型”を決める
- 余計なフィールドを自動で落とす(情報漏えい防止)
/docsに「この API はこういう JSON を返します」と表示してくれる
といったメリットが得られます。
リクエストモデルが「受け取る側の設計図」だとしたら、
レスポンスモデルは「外に出してよい形の設計図」です。
FastAPI におけるレスポンスモデルの基本
response_model 引数で「返り値の型」を宣言する
FastAPI では、エンドポイントを定義するときに response_model を指定します。
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel):
id: int
name: str
@app.get("/items/{item_id}", response_model=Item)
def get_item(item_id: int):
return Item(id=item_id, name=f"item-{item_id}")
Pythonここでのポイントは、
Itemモデルが「レスポンスの形」の定義response_model=Itemが「このエンドポイントは Item を返すべき」という宣言
になっていることです。
実際の JSON レスポンスは、例えば /items/1 に対して
{
"id": 1,
"name": "item-1"
}
のようになります。
関数の戻り値として Item インスタンスを返してもいいし、{"id": ..., "name": ...} のような dict を返しても、
最終的に FastAPI が Item の形に合わせて整形してくれます。
リストやネスト構造も response_model で表現できる
複数件を返したいときは、リスト型を使います。
from typing import List
class Item(BaseModel):
id: int
name: str
@app.get("/items", response_model=List[Item])
def list_items():
return [
{"id": 1, "name": "Apple"},
{"id": 2, "name": "Banana"}
]
Pythonresponse_model=List[Item] と書くことで、
- レスポンスは「Item 型の要素を持つリスト」
/docsにも「配列で返るよ」という形で表示
されます。
なぜレスポンスモデルが必要なのか(辞書をそのまま返すのと何が違う?)
「内部の情報全部」を返してしまう事故を防ぐ
ありがちなパターンとして、
def get_user_from_db(user_id: int) -> dict:
return {
"id": user_id,
"name": "Taro",
"email": "taro@example.com",
"password_hash": "xxxxx",
}
@app.get("/users/{user_id}")
def get_user(user_id: int):
return get_user_from_db(user_id)
Pythonのように書いてしまうと、password_hash のような「外に出したくない情報」まで
そのまま JSON としてクライアントに返ってしまうことがあります。
レスポンスモデルを使うと、「外に出してよいフィールド」だけを選別できます。
from pydantic import BaseModel
class UserPublic(BaseModel):
id: int
name: str
email: str
@app.get("/users/{user_id}", response_model=UserPublic)
def get_user(user_id: int):
user = get_user_from_db(user_id)
return user # password_hash があっても、レスポンスからは落ちる
Pythonget_user_from_db が返した dict には password_hash が含まれていても、
FastAPI は UserPublic に存在しないフィールドを自動で除外します。
これがレスポンスモデルの強力なポイントです。
- 内部では「いろいろ持っている」
- 外には「モデルで定義されたものだけ」
という“ふるい”を response_model が担ってくれます。
「返り値の形」をきちんと固定することで、フロント側が楽になる
レスポンスモデルを定義しておくと、
API を使う側(フロントエンドや別サービス)が
- どんなキーが必ずあるか
- どの型(数値か文字列か)なのか
を事前に知ることができます。
FastAPI の /docs を開くと、
- Request body
- Responses → 200 → Schema(=レスポンスモデル)
が表示されるので、
「この API は成功するとこういう JSON を返すんだな」と一目で分かります。
レスポンスモデルをサボると、
- 場合によってフィールドが増えたり減ったり
- 型がブレる(数値と文字列が混じる)
といった“ブレた API”になりやすく、
利用側が壊れやすくなります。
内部モデルとレスポンスモデルを分ける(本当に大事な設計ポイント)
内部では「何でもあり」、外では「制限された形」にする
現実のアプリでは、
DB モデルや内部用モデル(ドメインモデル)には、
外に出したくない情報も含まれます。
例えば、ユーザーのモデルを考えてみます。
from pydantic import BaseModel
class UserInternal(BaseModel):
id: int
name: str
email: str
password_hash: str
is_admin: bool
Pythonこれをそのまま response_model にしてしまうと、password_hash や is_admin まで外に出てしまいます。
そこで、外向けの「公開用モデル」を別に用意します。
class UserPublic(BaseModel):
id: int
name: str
email: str
Pythonエンドポイントでは、内部モデルを返しつつ、response_model=UserPublic を指定します。
@app.get("/users/{user_id}", response_model=UserPublic)
def get_user(user_id: int):
user = get_user_internal(user_id) # UserInternal を返す想定
return user
PythonFastAPI は、
- 戻り値を一度 UserPublic として解釈
- UserPublic にないフィールド(password_hash, is_admin)は削除
した上で JSON を返します。
こうすることで、
- 内部ロジックや DB の都合でモデルが少し変わっても
- 外向けのレスポンスの型は変えない
という設計ができます。
この「内部モデルと公開モデルの分離」は、
アプリが大きくなればなるほど効いてきます。
レスポンスモデルを「API バージョンごとの契約」として扱う
API を長く運用していると、「レスポンスの形式を変えたい」場面が出てきます。
そのときに、
- 内部構造は自由に変えつつ
- 古いクライアント向けには、昔のレスポンスモデルを維持
- 新しいクライアント向けには、新しいレスポンスモデルを使う
という戦略が取れます。
バージョンごとにレスポンスモデル(UserV1, UserV2 など)を用意しておけば、/v1/users/... と /v2/users/... で違うモデルを response_model にするだけで済みます。
レスポンスモデルとバリデーション(「出す側」にも型の責任を持たせる)
response_model に合わない構造・型を返そうとしたら?
レスポンスモデルは、「出ていくデータ」にも型制約をかけます。
例えば、
class Item(BaseModel):
id: int
price: float
@app.get("/items/{item_id}", response_model=Item)
def get_item(item_id: int):
return {"id": "abc", "price": "100"}
Pythonこのように、id を文字列 "abc" にしてしまうと、
Pydantic が「int に変換できない」と判断してエラーになります。
多くの場合、FastAPI は必要に応じて型変換を試みますが、
どうしても無理な場合はエラーになります。
これによって、
- 内部コードのバグ(型の間違い)
- 間違ったフィールド名や構造
が早期に発覚します。
リクエスト側だけでなく、
レスポンス側にも「型の約束」を課すことで、
API 全体の信頼性が上がります。
一部だけ違う形で返したいとき(例:エラー時の専用モデル)
成功時だけでなく、
- 特定の成功パターン
- 部分成功
- エラー時
などに専用のレスポンスモデルを使いたいときもあります。
例えば、「バリデーションに通った件数」と「失敗した件数」を返すバッチ API。
from pydantic import BaseModel
class BatchResult(BaseModel):
success_count: int
failure_count: int
@app.post("/batch", response_model=BatchResult)
def run_batch():
# 何らかの処理...
return {"success_count": 10, "failure_count": 2}
Pythonこうすると、
クライアント側は「この API は常に success_count と failure_count を返す」と期待できます。
エラー時は HTTP ステータスコードとエラーモデルを組み合わせて
設計することもできますが、
まずは「成功時の JSON の形を固定する」だけでも大きな価値があります。
ネスト・配列・ラッパー型のレスポンスモデル(少しステップアップ)
結果リスト+メタ情報(total_count など)を返す
実務では、
- items: 検索結果のリスト
- total_count: 全件数
- limit / offset: ページング情報
のような「ラッパー JSON」を返すことがよくあります。
これもレスポンスモデルで表現できます。
from typing import List
from pydantic import BaseModel
class Item(BaseModel):
id: int
name: str
class ItemListResponse(BaseModel):
total_count: int
items: List[Item]
@app.get("/items", response_model=ItemListResponse)
def list_items():
fake_items = [
{"id": 1, "name": "Apple"},
{"id": 2, "name": "Banana"},
]
return {
"total_count": 2,
"items": fake_items
}
Pythonこれで、
{
"total_count": 2,
"items": [
{"id": 1, "name": "Apple"},
{"id": 2, "name": "Banana"}
]
}
という形を「契約」として固定できます。
検索APIや一覧APIで、
- 結果本体
- 件数・ページング情報
をまとめて返したいときには、
こういう「ラッパー型レスポンスモデル」が役立ちます。
まとめ(レスポンスモデルは「外に見せる顔」をデザインするもの)
FastAPI におけるレスポンスモデルの役割を整理すると、こうなります。
レスポンスモデルは、「この API はこういう JSON を返す」という外向けの約束(契約)を、Pydantic モデルで表現したもの。response_model=... を指定することで、余計なフィールドを自動で落とし、出してよい情報だけをクライアントに返せる(情報漏えいを防げる)。
内部モデル(UserInternal)と公開モデル(UserPublic)を分けることで、「内部の構造は自由に変えながら、外に見せる形は安定させる」設計ができる。
レスポンス側にも型バリデーションがかかるので、「返す JSON の形や型の間違い」を早期に検知でき、フロントや他サービスから使いやすい API になる。
リスト・ネスト・ラッパー型(total_count+items など)もレスポンスモデルで表現することで、検索結果や一覧、集計結果などの複雑なレスポンスも見通しよく設計できる。
