本文 AI 產出,尚未審核

FastAPI – API 文件(OpenAPI / Swagger / ReDoc)

主題:responses 結構定義


簡介

FastAPI 中,API 文件是自動產生的 OpenAPI(亦稱 Swagger)規格,開發者只要寫好路由與型別,就能即時得到完整、互動式的文件介面(Swagger UI、ReDoc)。
然而,要讓文件「真的」有用,僅靠 response_model 還不夠——我們還需要明確描述 不同狀態碼錯誤訊息結構、以及 自訂回傳格式。這些資訊全部透過路由的 responses 參數來定義。

正確使用 responses 不只提升文件的可讀性,還能:

  • 讓前端或第三方服務在開發階段即知道每個 API 可能返回的資料結構。
  • 減少因未說明錯誤格式而造成的溝通成本。
  • 在測試工具(如 pytesthttpx)中提供型別提示,提升測試效率。

以下將從概念說明、實作範例,到常見陷阱與最佳實踐,完整介紹 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 使用 PlainTextResponseresponses 中的 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())
  • 透過 modelexample,文件會自動呈現每個錯誤的 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/plainapplication/pdf)。
使用 model 時忘記匯入 Pydantic 類別 會在啟動時拋出 NameError 確保模型已在同一檔案或正確 import。
狀態碼寫成字串或整數不一致 OpenAPI 規範要求鍵為字串,FastAPI 會自動轉換,但若混用會讓 IDE 顯示警告。 建議統一使用 整數200404)或 字串"200"),保持風格一致。
同一端點同時使用 response_modelresponses 中的 model,卻忘記 response_model_exclude_unset 可能導致回傳的 JSON 包含未設定的欄位,文件與實際不符。 若需要排除未設定欄位,使用 response_model_exclude_unset=True
範例資料過於簡略 Swagger UI 只顯示一個 example,開發者無法了解完整結構。 提供 多個 examples,或在 example 中加入完整的 JSON 結構。

最佳實踐

  1. 一律為每個非 2xx 狀態碼提供 modelexample,讓文件完整。
  2. 使用 status_code 參數(如 @app.get(..., status_code=200))配合 responses,保證文件與實際回傳一致。
  3. 將錯誤模型集中管理(例如 class HTTPError(BaseModel): detail: str),減少重複程式碼。
  4. 在測試階段驗證 OpenAPI 產出,可使用 client.get("/openapi.json") 確認 responses 正確寫入。
  5. 針對檔案或二進位回傳,務必在 responses 中明確指定 media_type,避免 Swagger UI 無法預覽。

實際應用場景

場景 為何需要 responses 範例
OAuth2 認證失敗 前端需要根據 401403 顯示不同的錯誤訊息。 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_modelmodelexampleexamples,可以在 Swagger UIReDoc 中呈現清晰、可互動的 API 說明。
  • 實務上,務必為每個非 2xx 狀態碼提供 模型或範例,並在需要時指定正確的 media type(JSON、plain text、PDF…)。
  • 避免常見陷阱、遵循最佳實踐,能讓團隊在前後端協作、測試與文件維護上事半功倍。

掌握了 responses 的寫法後,您的 FastAPI 專案就能產出符合企業級需求的自動化 API 文件,讓開發、測試、部署全流程都更順暢、更可靠。祝開發愉快!