Response Model — Precise Control of API Output
レスポンスモデルは舞台の照明のようなものです。観客に見せるべきエリアだけを照らし, 舞台裏の機材 (内部フィールド)は暗いままにしておくことで, 美しさと安全性の両方を確保します。
1. 学ぶ内容
response_modelの基本:未宣言フィールドの自動フィルタリング, シリアライズ制御response_model_exclude/response_model_include:フィールドレベルのきめ細かいフィルタリング- マルチレスポンスモデル:単一エンドポイントで異なるモデルを返す (
Union/リストレスポンス) response_model_by_aliasとresponse_model_exclude_unset:実践的なティップス- Alice シナリオ:PriceTracker 価格レスポンス — 公開版 (卸売価格を非表示)vs 管理者版 (完全な価格チェーン)
2. Alice のリアルストーリー
(1) ペインポイント:卸売価格が競合に漏洩
Alice の PriceTracker は商品の小売価格と卸売価格を保存しています。ある日, Bob がフロントエンドから商品詳細をリクエストしたところ, API が卸売価格も返してしまいました。競合が API をスクレイピングしてすべての卸売価格情報を取得し, Alice の顧客は非常に不満になりました。問題の根本原因は, Flask にレスポンスフィルタリング機構がなく, ORM オブジェクトの全フィールドがそのままシリアライズされて返されていたことです。
(2) response_model の解決策
FastAPI の response_model 宣言的フィルタリング — モデルで宣言されたフィールドのみを返し, 未宣言のフィールドは自動的に除外し, スキーマレベルでデータ漏洩を防ぎます。
from fastapi import FastAPI
from pydantic import BaseModel
class ProductPublic(BaseModel):
id: int
name: str
retail_price: float # 小売価格のみ
class ProductAdmin(BaseModel):
id: int
name: str
retail_price: float
wholesale_price: float # 卸売価格を含む
app = FastAPI()
@app.get("/products/{id}", response_model=ProductPublic)
async def get_product_public(id: int):
# DB に wholesale_price があっても, response_model がフィルタリング
return {"id": id, "name": "Widget", "retail_price": 29.99, "wholesale_price": 15.0}
(3) 成果
レスポンスフィルタリングは「手動でフィールドを削除する」から「宣言されたモデルに基づく自動フィルタリング」に変わり, 卸売価格漏洩の問題が完全に解消されました。公開 API は ProductPublic で宣言されたフィールドのみを返し, 管理 API は ProductAdmin で完全なデータを返します。
3. response_model の基本
(1) 自動フィルタリングの原理
flowchart LR
A[ORM オブジェクト] --> B{response_model}
B -->|宣言されたフィールド| C[JSON レスポンス]
B -->|未宣言のフィールド| D[フィルタリング済み]
subgraph ProductPublic
E[id]
F[name]
G[retail_price]
end
subgraph Hidden
H[wholesale_price]
I[cost_price]
J[supplier_id]
end
(1) ▶ サンプル:response_model による自動フィルタリング
from fastapi import FastAPI
from pydantic import BaseModel
class ProductResponse(BaseModel):
id: int
name: str
category: str
# 追加フィールドを含む模擬データベースレコード
DB_PRODUCT = {
"id": 1,
"name": "Widget",
"category": "electronics",
"cost_price": 8.50, # レスポンスに含めるべきではない
"supplier_id": 42, # レスポンスに含めるべきではない
}
app = FastAPI()
@app.get("/products/{product_id}", response_model=ProductResponse)
async def get_product(product_id: int):
# cost_price と supplier_id は自動フィルタリングされる
return DB_PRODUCT
出力:
{"id": 1, "name": "Widget", "category": "electronics"}
(2) response_model と戻り値型の比較
| 方法 | フィルタリング動作 | ドキュメント生成 | 推奨シナリオ |
|---|---|---|---|
response_model=X |
自動フィルタリング | あり | 常に推奨 |
戻り値型 -> X |
フィルタリングなし | あり | 型アノテーションのみ |
| 宣言なし | フィルタリングなし | なし | 非推奨 |
(2) ▶ サンプル:response_model と戻り値型の比較
from fastapi import FastAPI
from pydantic import BaseModel
class ProductBrief(BaseModel):
id: int
name: str
app = FastAPI()
@app.get("/demo/model", response_model=ProductBrief)
async def with_response_model():
# response_model はフィルタリング:レスポンスには id と name のみ
return {"id": 1, "name": "Widget", "secret": "hidden"}
@app.get("/demo/type") -> ProductBrief
async def with_return_type():
# 戻り値型はフィルタリングしない:secret フィールドが漏洩!
return {"id": 1, "name": "Widget", "secret": "leaked"}
出力:
# 関数定義成功
4. フィールドレベルのきめ細かいフィルタリング
(1) exclude と include
すべてのシナリオで新しいモデルを作成したくない場合, response_model_exclude と response_model_include を使用して動的にフィールドをフィルタリングできます。
(1) ▶ サンプル:exclude で機密フィールドを除外
from fastapi import FastAPI
from pydantic import BaseModel, Field
class ProductFull(BaseModel):
id: int
name: str
retail_price: float
wholesale_price: float = Field(exclude=True) # デフォルトで除外
cost_price: float = Field(exclude=True)
supplier: str = Field(exclude=True)
app = FastAPI()
@app.get("/products/{id}", response_model=ProductFull)
async def get_product(id: int):
return {
"id": id,
"name": "Widget",
"retail_price": 29.99,
"wholesale_price": 15.0,
"cost_price": 8.50,
"supplier": "Acme Corp",
}
出力:
INFO: 127.0.0.1:50123 - "GET /api/items HTTP/1.1" 200 OK
出力 (wholesale_price, cost_price, supplier はフィルタリング済み):
{"id": 1, "name": "Widget", "retail_price": 29.99}
(2) ▶ サンプル:response_model_exclude 動的除外
from fastapi import FastAPI
from pydantic import BaseModel
class ProductFull(BaseModel):
id: int
name: str
retail_price: float
wholesale_price: float
cost_price: float
app = FastAPI()
@app.get(
"/products/{id}",
response_model=ProductFull,
response_model_exclude={"wholesale_price", "cost_price"},
)
async def get_product_public(id: int):
return {
"id": id, "name": "Widget",
"retail_price": 29.99, "wholesale_price": 15.0, "cost_price": 8.50,
}
@app.get(
"/admin/products/{id}",
response_model=ProductFull,
)
async def get_product_admin(id: int):
return {
"id": id, "name": "Widget",
"retail_price": 29.99, "wholesale_price": 15.0, "cost_price": 8.50,
}
出力:
# 関数定義成功
| フィルタリング方法 | 対象シナリオ | 粒度 |
|---|---|---|
response_model=ModelA |
異なる視点からの異なるモデル | モデルレベル |
response_model_exclude={fields} |
少数のフィールドを除外 | フィールドレベル |
response_model_include={fields} |
少数のフィールドのみ含める | フィールドレベル |
Field(exclude=True) |
特定のフィールドを除外 | フィールド定義レベル |
5. マルチレスポンスモデルと高度なテクニック
(1) 同じエンドポイントで異なるレスポンス
(1) ▶ サンプル:Union レスポンスモデル
from fastapi import FastAPI
from pydantic import BaseModel
from typing import Union
class ProductFound(BaseModel):
id: int
name: str
price: float
class ProductNotFound(BaseModel):
error: str
product_id: int
app = FastAPI()
@app.get("/products/{id}", response_model=Union[ProductFound, ProductNotFound])
async def search_product(id: int):
if id == 1:
return ProductFound(id=1, name="Widget", price=9.99)
return ProductNotFound(error="Not found", product_id=id)
出力:
# 関数定義成功
(2) ▶ サンプル:リストレスポンスモデル
from fastapi import FastAPI
from pydantic import BaseModel
class PriceEntry(BaseModel):
product_id: int
price: float
recorded_at: str
app = FastAPI()
@app.get("/products/{id}/prices", response_model=list[PriceEntry])
async def get_price_history(id: int):
return [
{"product_id": id, "price": 29.99, "recorded_at": "2026-01-01"},
{"product_id": id, "price": 24.99, "recorded_at": "2026-02-01"},
{"product_id": id, "price": 34.99, "recorded_at": "2026-03-01"},
]
出力:
# 関数定義成功
(2) exclude_unset と exclude_none
(3) ▶ サンプル:exclude_unset で値のあるフィールドのみ返す
from fastapi import FastAPI
from pydantic import BaseModel
from typing import Optional
class ProductUpdate(BaseModel):
name: Optional[str] = None
category: Optional[str] = None
price: Optional[float] = None
app = FastAPI()
@app.get(
"/products/{id}",
response_model=ProductUpdate,
response_model_exclude_unset=True,
)
async def get_product_partial(id: int):
# name のみ設定され, category と price は None のまま
return ProductUpdate(name="Updated Widget")
出力:
INFO: 127.0.0.1:50123 - "GET /api/items HTTP/1.1" 200 OK
出力 (値のあるフィールドのみ含む):
{"name": "Updated Widget"}
| オプション | 効果 | ユースケース |
|---|---|---|
response_model_exclude_unset=True |
未設定のフィールドを除外 | PATCH レスポンス:更新されたフィールドのみ返す |
response_model_exclude_none=True |
値が None のフィールドを除外 | null 値を削除してレスポンスを簡潔に |
response_model_exclude_defaults=True |
デフォルト値を使用するフィールドを除外 | 冗長なデフォルト値を削除 |
response_model_by_alias=True |
エイリアスを使用してフィールドを出力 | フロントエンドが camelCase 命名を要求 |
(4) ▶ サンプル:response_model_by_alias
from fastapi import FastAPI
from pydantic import BaseModel, Field, ConfigDict
class ProductResponse(BaseModel):
model_config = ConfigDict(populate_by_name=True)
id: int
product_name: str = Field(alias="productName")
unit_price: float = Field(alias="unitPrice", description="Price in USD")
app = FastAPI()
@app.get("/products/{id}", response_model=ProductResponse, response_model_by_alias=True)
async def get_product(id: int):
return {"id": id, "productName": "Widget", "unitPrice": 9.99}
出力:
INFO: 127.0.0.1:50123 - "GET /api/items HTTP/1.1" 200 OK
出力 (エイリアスをフィールド名に使用):
{"id": 1, "productName": "Widget", "unitPrice": 9.99}
❓ よくある質問
response_model と戻り値型アノテーションの違いは何ですか?response_model は未宣言のフィールドをフィルタリングし, 出力をバリデーションします。戻り値型アノテーションは OpenAPI ドキュメント生成にのみ影響し, フィールドのフィルタリングは行いません。常に response_model を使用してください。exclude_unset はどのようなシナリオで最も役立ちますか?{"nested_model": {"secret_field"}} を使用してネストされたフィールドを除外できます。response_model=list[ItemModel] を使用します。FastAPI はリスト内の各要素をモデルに従って自動的にフィルタリングします。response_model はパフォーマンスに影響しますか?orjson でシリアライズを最適化できます。📖 まとめ
response_modelは未宣言のフィールドを自動的にフィルタリングし, スキーマレベルで機密データの漏洩を防ぎますresponse_model_exclude/response_model_includeは新しいモデルを作成せずにフィールドレベルのきめ細かい制御を提供しますUnionレスポンスモデルは同じエンドポイントから異なる構造を返すことをサポートし, リストレスポンスはlist[Model]を参照してくださいresponse_model_exclude_unsetは値が割り当てられたフィールドのみを返し, PATCH シナリオに適していますresponse_model_by_alias=Trueはエイリアスを使用してレスポンスを出力し, フロントエンドの camelCase 命名規約に対応します
📝 練習問題
- 基本問題 (難易度 ⭐):
ProductPublicモデル (id, name, price)を作成し,response_modelを使用してエンドポイントがこの3つのフィールドのみを返すようにしてください。cost_priceを含む dict を返しても情報は漏洩しません。ヒント:@app.get(..., response_model=ProductPublic) - 応用問題 (難易度 ⭐⭐):PriceTracker の2つのエンドポイントを作成してください —
wholesale_priceを非表示にする公開エンドポイントと, 全フィールドを表示する管理者エンドポイントです。response_model_excludeで実装してください。ヒント:response_model_exclude={"wholesale_price"} - チャレンジ問題 (難易度 ⭐⭐⭐):オプショナルフィールド (description, image_url など)を持つ
ProductDetailモデルを作成し,response_model_exclude_unset=Trueを使用して, エンドポイントがクライアントから提供されたフィールドのみを返し, 空の値が JSON から省略されるようにしてください。ヒント:Optional[str] = None+response_model_exclude_unset=True
---|



