FastAPI – API 文件(OpenAPI / Swagger / ReDoc)
主題:responses 結構定義
簡介
在 FastAPI 中,API 文件是自動產生的 OpenAPI(亦稱 Swagger)規格,開發者只要寫好路由與型別,就能即時得到完整、互動式的文件介面(Swagger UI、ReDoc)。
然而,要讓文件「真的」有用,僅靠 response_model 還不夠——我們還需要明確描述 不同狀態碼、錯誤訊息結構、以及 自訂回傳格式。這些資訊全部透過路由的 responses 參數來定義。
正確使用 responses 不只提升文件的可讀性,還能:
- 讓前端或第三方服務在開發階段即知道每個 API 可能返回的資料結構。
- 減少因未說明錯誤格式而造成的溝通成本。
- 在測試工具(如
pytest、httpx)中提供型別提示,提升測試效率。
以下將從概念說明、實作範例,到常見陷阱與最佳實踐,完整介紹 responses 的寫法與應用。
核心概念
1️⃣ responses 基本語法
responses 是路由裝飾器(@app.get、@app.post…)的可選參數,接受一個 字典,鍵為 HTTP 狀態碼(字串或整數),值為描述該狀態碼回傳內容的 OpenAPI 結構。最常見的寫法如下:
from fastapi import FastAPI, HTTPException
app = FastAPI()
@app.get("/items/{item_id}", responses={
200: {"description": "成功取得單一商品"},
404: {"description": "找不到商品", "content": {"application/json": {"example": {"detail": "Item not found"}}}},
})
async def read_item(item_id: int):
if item_id == 42:
return {"item_id": item_id, "name": "神奇商品"}
raise HTTPException(status_code=404, detail="Item not found")
200只給了description,FastAPI 會自動使用回傳的 Python 物件推斷 schema。404則明確提供content,告訴文件「錯誤時回傳的是application/json,範例為{"detail": "Item not found"}」。
2️⃣ 搭配 response_model 使用
response_model 用來描述 成功 時的資料結構;responses 則補足 非 2xx 的情況。兩者可以同時存在,互不衝突:
from pydantic import BaseModel
class Item(BaseModel):
item_id: int
name: str
class ErrorMessage(BaseModel):
detail: str
@app.get(
"/items/{item_id}",
response_model=Item,
responses={
404: {
"model": ErrorMessage,
"description": "找不到商品",
}
},
)
async def get_item(item_id: int):
if item_id != 42:
raise HTTPException(status_code=404, detail="Item not found")
return Item(item_id=item_id, name="神奇商品")
model讓 FastAPI 自動產生 JSON Schema,同時在 Swagger UI 中顯示錯誤回傳的欄位。
3️⃣ 自訂回傳內容(非 JSON)
有時候 API 需要回傳 檔案、純文字 或 HTML。此時 content 必須明確指定 media_type,並提供 example(或 examples):
from fastapi.responses import PlainTextResponse
@app.get(
"/ping",
responses={
200: {
"description": "簡易健康檢查",
"content": {
"text/plain": {
"example": "pong"
}
},
}
},
response_class=PlainTextResponse,
)
async def ping():
return "pong"
response_class告訴 FastAPI 使用PlainTextResponse;responses中的content則讓文件正確顯示text/plain。
4️⃣ 多種錯誤狀態的範例
在實務系統裡,一個端點可能因 驗證失敗、授權不足、資料衝突 等原因返回不同的錯誤。responses 可以一次列出多個狀態碼:
@app.post(
"/users/",
response_model=UserOut,
responses={
201: {"description": "使用者建立成功"},
400: {
"description": "資料驗證失敗",
"content": {"application/json": {"example": {"detail": "Email already exists"}}},
},
401: {"description": "未授權"},
409: {
"description": "使用者已存在",
"model": ErrorMessage,
},
},
)
async def create_user(user: UserCreate):
# 假設檢查邏輯...
if not authorized():
raise HTTPException(status_code=401, detail="Unauthorized")
# 其他處理
return UserOut(**user.dict())
- 透過
model或example,文件會自動呈現每個錯誤的 JSON schema,讓前端開發者一眼就能知道要捕捉什麼欄位。
5️⃣ 使用 examples 提供多個範例
有時候同一錯誤會有不同情境(例如驗證錯誤可能是「欄位缺失」或「格式錯誤」),可以使用 examples:
@app.put(
"/items/{item_id}",
responses={
422: {
"description": "驗證失敗",
"content": {
"application/json": {
"examples": {
"missing_field": {
"summary": "缺少必填欄位",
"value": {"detail": [{"loc": ["body", "name"], "msg": "field required", "type": "value_error.missing"}]}
},
"invalid_type": {
"summary": "欄位類型錯誤",
"value": {"detail": [{"loc": ["body", "price"], "msg": "value is not a valid float", "type": "type_error.float"}]}
},
}
}
},
}
},
)
async def update_item(item_id: int, item: ItemUpdate):
# 處理更新邏輯...
return {"msg": "updated"}
examples讓 Swagger UI 顯示 多個 可切換的範例,對於 API 使用者非常友善。
程式碼範例(實用)
以下彙整 5 個常見情境的完整範例,您可以直接 copy & paste 到自己的專案中。
範例 1:基本 responses + response_model
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
app = FastAPI()
class Product(BaseModel):
id: int
name: str
price: float
class NotFound(BaseModel):
detail: str
@app.get(
"/products/{pid}",
response_model=Product,
responses={404: {"model": NotFound, "description": "找不到商品"}},
)
async def get_product(pid: int):
if pid != 1:
raise HTTPException(status_code=404, detail="Product not found")
return Product(id=pid, name="Apple", price=3.5)
範例 2:回傳純文字與自訂 example
from fastapi.responses import PlainTextResponse
@app.get(
"/health",
response_class=PlainTextResponse,
responses={
200: {
"description": "服務健康檢查",
"content": {"text/plain": {"example": "OK"}},
}
},
)
async def health_check():
return "OK"
範例 3:多重錯誤範例(400、401、409)
class UserCreate(BaseModel):
email: str
password: str
class ErrorMsg(BaseModel):
detail: str
@app.post(
"/users/",
response_model=UserCreate,
responses={
201: {"description": "使用者建立成功"},
400: {"model": ErrorMsg, "description": "欄位驗證失敗"},
401: {"description": "未授權"},
409: {"model": ErrorMsg, "description": "使用者已存在"},
},
)
async def register(user: UserCreate):
if not authorized():
raise HTTPException(status_code=401, detail="Unauthorized")
if user_exists(user.email):
raise HTTPException(status_code=409, detail="User already exists")
# 假設成功建立
return user
範例 4:使用 examples 描述 422 錯誤
@app.post(
"/orders/",
responses={
422: {
"description": "驗證失敗",
"content": {
"application/json": {
"examples": {
"missing_field": {
"summary": "缺少必填欄位",
"value": {"detail": [{"loc": ["body", "quantity"], "msg": "field required", "type": "value_error.missing"}]},
},
"invalid_type": {
"summary": "型別錯誤",
"value": {"detail": [{"loc": ["body", "price"], "msg": "value is not a valid float", "type": "type_error.float"}]},
},
}
}
},
}
},
)
async def create_order(order: dict):
# 處理邏輯...
return {"msg": "order created"}
範例 5:檔案下載的 responses 定義
from fastapi.responses import FileResponse
@app.get(
"/reports/{report_id}",
responses={
200: {
"description": "PDF 報表檔案",
"content": {"application/pdf": {"example": "binary data"}},
},
404: {"description": "報表不存在"},
},
)
async def download_report(report_id: int):
path = f"/tmp/report_{report_id}.pdf"
if not os.path.exists(path):
raise HTTPException(status_code=404, detail="Report not found")
return FileResponse(path, media_type="application/pdf", filename=f"report_{report_id}.pdf")
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方案 |
|---|---|---|
忘記在 responses 中加入 content |
只寫 description,文件仍會顯示 application/json,但實際回傳可能是 text/plain 或檔案,造成前端誤解。 |
必須同時指定 content 與正確的 media_type(如 text/plain、application/pdf)。 |
使用 model 時忘記匯入 Pydantic 類別 |
會在啟動時拋出 NameError。 |
確保模型已在同一檔案或正確 import。 |
| 狀態碼寫成字串或整數不一致 | OpenAPI 規範要求鍵為字串,FastAPI 會自動轉換,但若混用會讓 IDE 顯示警告。 | 建議統一使用 整數(200、404)或 字串("200"),保持風格一致。 |
同一端點同時使用 response_model 與 responses 中的 model,卻忘記 response_model_exclude_unset |
可能導致回傳的 JSON 包含未設定的欄位,文件與實際不符。 | 若需要排除未設定欄位,使用 response_model_exclude_unset=True。 |
| 範例資料過於簡略 | Swagger UI 只顯示一個 example,開發者無法了解完整結構。 |
提供 多個 examples,或在 example 中加入完整的 JSON 結構。 |
最佳實踐
- 一律為每個非 2xx 狀態碼提供
model或example,讓文件完整。 - 使用
status_code參數(如@app.get(..., status_code=200))配合responses,保證文件與實際回傳一致。 - 將錯誤模型集中管理(例如
class HTTPError(BaseModel): detail: str),減少重複程式碼。 - 在測試階段驗證 OpenAPI 產出,可使用
client.get("/openapi.json")確認responses正確寫入。 - 針對檔案或二進位回傳,務必在
responses中明確指定media_type,避免 Swagger UI 無法預覽。
實際應用場景
| 場景 | 為何需要 responses |
範例 |
|---|---|---|
| OAuth2 認證失敗 | 前端需要根據 401 或 403 顯示不同的錯誤訊息。 |
responses={401: {"model": ErrorMsg, "description": "Token 無效"}, 403: {"model": ErrorMsg, "description": "權限不足"}} |
| 批次匯入 CSV | 成功回傳匯入結果(200),失敗時回傳欄位錯誤清單(422)。 |
使用 examples 列出「缺少欄位」與「格式錯誤」兩種情況。 |
| 下載報表 | 回傳 PDF 檔案(200)與找不到檔案的錯誤(404)。 |
參考上方「檔案下載」範例。 |
| 即時聊天 API | 成功回傳訊息(201),若頻道不存在則回傳 404,若訊息內容不合法則回傳 400。 |
為每個狀態碼提供 model,確保前端 UI 能正確顯示錯誤。 |
| 第三方 webhook | 第三方系統可能因為驗證失敗回傳 401,或因為服務暫停回傳 503。 |
在 responses 中加入 503 的說明與範例,讓合作夥伴了解重試策略。 |
總結
responses是 FastAPI 讓 OpenAPI 文件 完整描述 各種回傳情境的關鍵工具。- 結合
response_model、model、example、examples,可以在 Swagger UI 與 ReDoc 中呈現清晰、可互動的 API 說明。 - 實務上,務必為每個非 2xx 狀態碼提供 模型或範例,並在需要時指定正確的 media type(JSON、plain text、PDF…)。
- 避免常見陷阱、遵循最佳實踐,能讓團隊在前後端協作、測試與文件維護上事半功倍。
掌握了 responses 的寫法後,您的 FastAPI 專案就能產出符合企業級需求的自動化 API 文件,讓開發、測試、部署全流程都更順暢、更可靠。祝開發愉快!