FastAPI 課程 – Pydantic 模型(Request / Response Models)
主題:Response Model 驗證
簡介
在使用 FastAPI 開發 Web API 時,最常被提及的優勢之一就是自動產生的資料驗證與文件(OpenAPI)說明。雖然大多數開發者在實作 Request Model 時已經相當熟悉,但 Response Model 的驗證同樣重要,因為它直接關係到:
- API 的正確性:保證回傳給前端或第三方服務的資料結構與型別符合預期。
- 安全性:避免不小心把內部敏感欄位洩漏給使用者。
- 文件一致性:FastAPI 會根據 Response Model 自動產生 Swagger UI,確保文件與實際回傳保持同步。
本篇文章將深入探討 Response Model 的概念、使用方式與常見陷阱,並提供多個實作範例,協助從初學者到中階開發者在專案中正確運用驗證機制。
核心概念
1. 為什麼需要 Response Model?
FastAPI 內部會在路由函式返回值前,先將資料 序列化(serialization)並 驗證(validation)一次。這個過程使用的就是 Pydantic 模型。若回傳的資料不符合模型定義,FastAPI 會拋出例外,讓開發者即時發現錯誤,而不是讓錯誤的 JSON 靜靜流向前端。
重點:Response Model 不僅是「文件」的來源,更是 執行時的防護網。
2. 基本語法
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class UserResponse(BaseModel):
id: int
username: str
email: str
is_active: bool = True # 預設值
@app.get("/users/{user_id}", response_model=UserResponse)
async def get_user(user_id: int):
# 假設從資料庫取得的資料是一個 dict
db_user = {"id": user_id, "username": "alice", "email": "alice@example.com"}
return db_user # FastAPI 會自動套用 UserResponse 進行驗證
response_model=UserResponse告訴 FastAPI 回傳資料必須符合UserResponse的結構。- 若
db_user少了is_active欄位,Pydantic 會自動補上預設值True。
3. 只回傳子集 – response_model_exclude_unset
有時候資料庫模型包含很多欄位(例如密碼、內部狀態),但 API 僅需要回傳公開資訊。可以透過 exclude 或 include 參數控制序列化行為。
class UserInDB(BaseModel):
id: int
username: str
email: str
hashed_password: str
is_active: bool
class UserPublic(BaseModel):
id: int
username: str
email: str
@app.get("/users/{user_id}", response_model=UserPublic)
async def get_user(user_id: int):
user = UserInDB(
id=user_id,
username="bob",
email="bob@example.com",
hashed_password="s3cr3t",
is_active=False,
)
# 直接回傳 UserInDB,FastAPI 會自動過濾掉未在 UserPublic 中的欄位
return user
4. 動態欄位 – response_model_include / response_model_exclude
有時候根據使用者權限或查詢參數,需要動態決定回傳哪些欄位。FastAPI 允許在路由裝飾器中使用 response_model_include 或 response_model_exclude。
@app.get(
"/items/{item_id}",
response_model=ItemResponse,
response_model_exclude={"internal_note"},
)
async def get_item(item_id: int):
item = {
"id": item_id,
"name": "Magic Wand",
"price": 99.9,
"internal_note": "Only for admin"
}
return item
5. 使用 list[Model] 或 Dict[str, Model]
當 API 回傳集合時,直接在 response_model 裡使用 Python 的型別提示即可。
from typing import List
class Product(BaseModel):
sku: str
name: str
price: float
@app.get("/products", response_model=List[Product])
async def list_products():
return [
{"sku": "A001", "name": "Notebook", "price": 3.5},
{"sku": "B002", "name": "Pen", "price": 1.2},
]
6. 自訂 JSON Encoder – json_encoders
如果模型中包含非 JSON 原生類型(如 datetime, UUID),可以在模型內部設定 json_encoders,讓 Pydantic 了解如何序列化。
from datetime import datetime
from uuid import UUID
from pydantic import BaseModel
class OrderResponse(BaseModel):
order_id: UUID
created_at: datetime
class Config:
json_encoders = {
datetime: lambda v: v.isoformat(),
UUID: lambda v: str(v),
}
程式碼範例
以下提供 5 個實用範例,從最簡單的回傳模型到進階的條件過濾與自訂序列化,幫助你快速上手。
範例 1:最基本的 Response Model
# file: main.py
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class SimpleResponse(BaseModel):
message: str
status: int = 200 # 預設值
@app.get("/ping", response_model=SimpleResponse)
async def ping():
return {"message": "pong"} # FastAPI 會自動補上 status=200
說明
- 只回傳一個簡單的字典,Pydantic 會自動填補
status欄位的預設值。 - 若回傳缺少
message,FastAPI 會回傳 500 錯誤,提醒開發者模型不匹配。
範例 2:隱藏敏感欄位(使用兩個模型)
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class UserDB(BaseModel):
id: int
username: str
email: str
hashed_password: str # 敏感資訊
class UserOut(BaseModel):
id: int
username: str
email: str
@app.get("/users/{uid}", response_model=UserOut)
async def get_user(uid: int):
# 模擬從資料庫取得完整資料
db_user = UserDB(
id=uid,
username="charlie",
email="charlie@example.com",
hashed_password="super_secret_hash"
)
return db_user # FastAPI 只會回傳 UserOut 定義的欄位
說明
UserDB包含密碼欄位,但response_model=UserOut只會序列化公開欄位。- 這樣的設計避免了 資訊外洩,同時保持程式碼的可讀性。
範例 3:條件排除欄位(依使用者權限)
from fastapi import FastAPI, Depends, HTTPException, status
from pydantic import BaseModel
from typing import Set
app = FastAPI()
class Article(BaseModel):
id: int
title: str
content: str
author_id: int
is_private: bool = False
def get_current_user():
# 假設回傳使用者 ID 與角色集合
return {"user_id": 42, "roles": {"reader"}}
@app.get(
"/articles/{article_id}",
response_model=Article,
response_model_exclude={"author_id"} # 預設排除作者 ID
)
async def read_article(article_id: int, user=Depends(get_current_user)):
article = Article(
id=article_id,
title="FastAPI 入門",
content="Lorem ipsum ...",
author_id=1,
is_private=False,
)
# 若使用者是作者或具有 admin 權限,回傳完整資料
if user["user_id"] == article.author_id or "admin" in user["roles"]:
return article # 此時不會被排除 author_id
# 否則只回傳已排除的欄位
return article
說明
- 使用
response_model_exclude排除author_id,除非符合特定條件再返回完整資訊。 - 透過
Depends取得當前使用者,展示 動態欄位控制 的實作方式。
範例 4:回傳列表與分頁資訊
from fastapi import FastAPI, Query
from pydantic import BaseModel
from typing import List
app = FastAPI()
class Product(BaseModel):
sku: str
name: str
price: float
class PaginatedResponse(BaseModel):
total: int
page: int
size: int
items: List[Product]
@app.get("/store/products", response_model=PaginatedResponse)
async def list_products(
page: int = Query(1, ge=1),
size: int = Query(10, ge=1, le=100)
):
# 假設有 250 筆商品
total_items = 250
start = (page - 1) * size
end = start + size
sample_items = [
{"sku": f"P{i:04d}", "name": f"商品{i}", "price": round(i * 1.23, 2)}
for i in range(start + 1, min(end + 1, total_items + 1))
]
return {
"total": total_items,
"page": page,
"size": size,
"items": sample_items,
}
說明
PaginatedResponse包含items欄位,其型別是List[Product],示範 巢狀模型 的使用。- 這種寫法讓 Swagger UI 自動產生清楚的回傳結構,前端開發者可以直接對照。
範例 5:自訂 JSON 編碼(datetime & UUID)
from fastapi import FastAPI
from pydantic import BaseModel
from datetime import datetime
from uuid import uuid4, UUID
from typing import List
app = FastAPI()
class Event(BaseModel):
id: UUID
name: str
start_time: datetime
class Config:
json_encoders = {
datetime: lambda v: v.strftime("%Y-%m-%d %H:%M:%S"),
UUID: lambda v: str(v),
}
@app.get("/events", response_model=List[Event])
async def get_events():
return [
Event(id=uuid4(), name="開幕式", start_time=datetime.utcnow()),
Event(id=uuid4(), name="技術講座", start_time=datetime.utcnow()),
]
說明
json_encoders讓datetime以自訂格式輸出,UUID直接轉成字串。- 若不設定編碼器,FastAPI 仍會自動處理,但輸出形式會是 ISO8601,這裡示範如何客製化。
常見陷阱與最佳實踐
| 陷阱 | 可能的後果 | 解決方式 / 建議 |
|---|---|---|
| 回傳的物件不是 Pydantic 模型 | FastAPI 仍會嘗試套用 response_model,但若結構不符會拋 ValidationError,導致 500 錯誤。 |
確保返回值是 dict、list 或 Pydantic 實例,或使用 jsonable_encoder 手動轉換。 |
使用 response_model_exclude_unset=True 卻忘記預設值 |
欄位在模型中有預設值,但因未在回傳資料中提供,會被排除,前端收到缺欄位的 JSON。 | 若欄位必須 always present,直接在模型裡設 default,或在回傳前手動加入。 |
過度使用 Any |
失去型別檢查與自動文件產生的好處。 | 盡量使用具體型別,若真的需要彈性,可使用 Union 或 Literal。 |
| 忘記排除敏感欄位 | 密碼、金鑰等資訊可能外洩。 | 使用 兩層模型(DB Model + Public Model)或 response_model_exclude。 |
| 大型回傳資料未分頁 | 造成效能瓶頸與記憶體問題。 | 加入分頁、限制 size、或使用 StreamingResponse。 |
| 自訂 Encoder 沒有考慮 None | None 會被編碼成字串 "None",造成前端解析錯誤。 |
在 encoder 中加入 if v is None: return None 的檢查。 |
最佳實踐
模型分層:
ModelInDB(完整資料)ModelCreate/ModelUpdate(請求資料)ModelOut(回傳資料)
使用
jsonable_encoder:在需要手動處理非 JSON 類型時,先呼叫jsonable_encoder,再回傳。from fastapi.encoders import jsonable_encoder @app.post("/items", response_model=ItemOut) async def create_item(item: ItemCreate): db_item = save_to_db(item) return jsonable_encoder(db_item)統一錯誤回傳結構:定義一個全域的
ErrorResponse,讓前端可以預期錯誤格式。class ErrorResponse(BaseModel): detail: str code: int @app.exception_handler(ValidationError) async def validation_exception_handler(request, exc): return JSONResponse( status_code=422, content=ErrorResponse(detail=str(exc), code=1001).dict(), )開啟
response_model_exclude_none=True:自動剔除值為None的欄位,減少不必要的欄位傳輸。@app.get("/profile", response_model=UserOut, response_model_exclude_none=True) async def get_profile(): ...測試 Response Model:使用
TestClient直接驗證回傳結構,確保模型不會在未來的程式碼變更中被破壞。from fastapi.testclient import TestClient client = TestClient(app) def test_get_user(): r = client.get("/users/1") assert r.status_code == 200 data = r.json() assert "hashed_password" not in data assert data["id"] == 1
實際應用場景
| 場景 | 為何需要 Response Model 驗證 | 示例 |
|---|---|---|
| 金融 API(返回交易明細) | 必須保證金額、時間戳記、交易狀態的正確型別,避免金額誤差或時間格式錯誤。 | 使用 Decimal、datetime,並自訂 json_encoders。 |
| 社交平台(使用者資料) | 隱私保護:不允許回傳密碼、email 驗證碼等敏感欄位。 | 兩層模型 + response_model_exclude。 |
| 電商系統(商品列表) | 需要分頁、價格格式統一、庫存狀態可選。 | PaginatedResponse 搭配 List[Product],使用 response_model_exclude_unset。 |
| IoT 後端(設備狀態) | 大量時間序列資料,必須確保每筆資料都有正確的 timestamp 與 value 型別。 |
使用 List[Reading],並在模型內設定 json_encoders 讓 datetime 以 ISO8601 輸出。 |
| 多語系內容平台(文章內容) | 回傳時根據請求語系過濾不同欄位(如 title_en, title_zh),且必須保證欄位一致性。 |
在路由中使用 response_model_include 動態決定回傳欄位。 |
總結
- Response Model 不僅是文件生成的依據,更是 資料安全與一致性的最後防線。
- 透過 Pydantic 的驗證機制,FastAPI 能在回傳前自動校正、補齊預設值、排除敏感欄位,讓 API 更可靠。
- 掌握
response_model,response_model_include/exclude,response_model_exclude_none, 以及自訂json_encoders,可以應對絕大多數實務需求。 - 記得在專案中採用 模型分層、統一錯誤回傳、測試驗證 等最佳實踐,避免常見陷阱,提升開發與維運效率。
希望本篇文章能幫助你在 FastAPI 專案裡建立安全、可維護且文件完整的 API 回傳機制。祝開發順利,持續寫出高品質的服務! 🚀