本文 AI 產出,尚未審核
FastAPI 教學:response_model_exclude_unset 與 exclude_defaults 的使用
簡介
在開發 API 時,回傳的資料結構往往需要與前端或第三方服務保持一致。
FastAPI 透過 Pydantic 的 BaseModel 讓我們可以在 response_model 中宣告回傳格式,同時提供了 exclude_unset、exclude_defaults 等參數,協助開發者自動過濾掉不必要的欄位,減少網路傳輸量、提升效能,也能避免洩漏敏感資訊。
本篇文章將深入說明 response_model_exclude_unset 與 exclude_defaults 兩個功能的差異、使用時機,以及在實務專案中如何正確運用它們。文章適合 FastAPI 初學者 以及 想要優化回傳結果的中階開發者,透過具體範例,你將能快速上手並避免常見陷阱。
核心概念
1. Pydantic BaseModel 與 dict() 轉換
FastAPI 依賴 Pydantic 來驗證與序列化資料。BaseModel 提供的 dict() 方法接受多個參數,常見的有:
| 參數 | 說明 |
|---|---|
exclude_unset |
只保留 已被設定(即非預設值)的欄位。未提供的欄位會被排除。 |
exclude_defaults |
只保留 值不等於欄位預設值 的欄位。即使欄位被明確指定為預設值,也會被排除。 |
exclude_none |
排除值為 None 的欄位。 |
重點:
response_model_exclude_unset與exclude_defaults其實是 FastAPI 在回傳時自動呼叫model.dict()所使用的參數,兩者的行為與 Pydantic 原生dict()完全相同。
2. response_model_exclude_unset
- 作用:當你在路由上設定
response_model_exclude_unset=True時,FastAPI 只會把 實際被填寫(或被 Pydantic 驗證過)的欄位序列化回傳。 - 適用情境:
- 部分更新(PATCH):只回傳客戶端送來的欄位,避免把整筆資料全部回傳。
- 大型模型:減少不必要的欄位,降低回傳負擔。
3. exclude_defaults
- 作用:在回傳時排除 等於模型預設值 的欄位。
- 適用情境:
- 預設值本身不具意義:例如
status: int = 0,若大多數資料都是 0,則可以不回傳。 - 節省流量:在需要傳遞大量資料且預設值佔多數時特別有用。
- 預設值本身不具意義:例如
4. 兩者的差異
| 條件 | exclude_unset |
exclude_defaults |
|---|---|---|
| 欄位未被提供(未設定) | 排除 | 保留(若有預設值) |
欄位被設定為 None |
排除(除非 exclude_none=False) |
保留(若預設值不是 None) |
| 欄位值等於預設值 | 保留(因為已被設定) | 排除 |
程式碼範例
以下範例均使用 Python(FastAPI)撰寫,請搭配 uvicorn 執行。
範例 1:基本模型與 exclude_unset
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel):
name: str
description: str | None = None
price: float
tax: float | None = None
@app.post("/items/", response_model=Item, response_model_exclude_unset=True)
async def create_item(item: Item):
# 假設只想回傳 client 真正提供的欄位
return item
說明
- 若 client 只傳
name與price,回傳結果會是:
{
"name": "Apple",
"price": 2.5
}
description與tax因為 未被設定,被exclude_unset排除。
範例 2:使用 exclude_defaults 排除預設值
class User(BaseModel):
username: str
is_active: bool = True # 預設為 True
role: str = "user"
@app.get("/users/{uid}", response_model=User, response_model_exclude_defaults=True)
async def get_user(uid: int):
# 假設從資料庫取回的資料全都是預設值
return User(username="alice")
回傳結果:
{
"username": "alice"
}
is_active與role因為等於預設值,被exclude_defaults自動過濾。
範例 3:同時使用 exclude_unset 與 exclude_defaults
@app.patch("/items/{item_id}",
response_model=Item,
response_model_exclude_unset=True,
response_model_exclude_defaults=True)
async def update_item(item_id: int, item: Item):
# 假設只更新部分欄位,且某些欄位使用了預設值
stored_item = {"name": "Banana", "price": 1.2, "tax": 0.1}
updated = {**stored_item, **item.dict(exclude_unset=True)}
return Item(**updated)
情境說明
- 客戶端只送
tax: 0(即預設值),exclude_unset=True會把tax包含在item.dict()中, - 但
exclude_defaults=True會在回傳前把tax: 0再度過濾掉,最終回傳只剩下原始的name、price。
範例 4:結合 exclude_none 與 exclude_unset
class Product(BaseModel):
id: int
name: str
description: str | None = None
tags: list[str] = []
@app.get("/products/{pid}",
response_model=Product,
response_model_exclude_unset=True,
response_model_exclude_none=True)
async def get_product(pid: int):
# DB 回傳的資料有 description 為 None
return Product(id=pid, name="Keyboard")
回傳結果:
{
"id": 1,
"name": "Keyboard"
}
description為None,被exclude_none=True排除;tags為預設空陣列,因為 已被設定(即使是空陣列),仍會保留,若想排除空陣列可自行在dict()時處理。
範例 5:自訂 response_model_include 與 response_model_exclude
@app.get("/orders/{oid}",
response_model=Item,
response_model_include={"name", "price"},
response_model_exclude={"tax"})
async def get_order(oid: int):
return Item(name="Book", price=12.99, tax=1.3, description="A novel")
回傳結果:
{
"name": "Book",
"price": 12.99
}
response_model_include明確指定只回傳name、price,response_model_exclude再次排除tax(即使不在include中也不會出現)。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方式 |
|---|---|---|
誤以為 exclude_unset 會排除 None |
exclude_unset 只關心「欄位是否被設定」,若欄位被設定為 None,仍會回傳。 |
若需要同時排除 None,加上 response_model_exclude_none=True。 |
| 預設值與實際值混淆 | exclude_defaults 只排除 等於預設值 的欄位,若在程式碼中手動指定相同值,仍會被排除。 |
確認業務邏輯是否真的不需要回傳該欄位;如需要回傳,考慮改用 exclude_unset 或自行調整模型預設值。 |
| 大量小型模型的組合 | 多層嵌套模型同時使用 exclude_unset,可能導致深層欄位意外被排除。 |
針對子模型使用 response_model_exclude_unset=False,或在返回前手動呼叫 model.dict() 進行精細控制。 |
| 測試環境與實際環境不一致 | 開發時使用 exclude_unset,但在生產環境中因為資料庫回傳的欄位全都有值,導致回傳內容變多。 |
在測試時模擬不同資料狀態,或在 API 文件中明確說明「哪些欄位可能會被省略」。 |
| 過度依賴排除機制 | 為了減少傳輸量而大量排除欄位,可能讓前端必須自行補足缺失的預設值,增加錯誤機率。 | 僅在確定前端能接受「缺失欄位」的情況下使用;若前端需要完整結構,建議保留欄位或在前端自行補值。 |
最佳實踐
- 先定義清晰的 API 規範:在 OpenAPI 文件中註明哪些欄位是「可選」且可能被省略。
- 根據業務需求選擇排除策略:
exclude_unset→ 只在「部分更新」或「大模型」時使用。exclude_defaults→ 在「大量預設值」且 不需要 前端知道的情況下使用。
- 結合
exclude_none:避免回傳null,提升前端資料處理的便利性。 - 使用自訂
dict():在極端情況下,直接在路由內部呼叫model.dict(exclude_unset=True, exclude_defaults=True),再回傳JSONResponse,可獲得最細緻的控制。 - 測試所有組合:使用
pytest或httpx測試不同請求情形,確保回傳結果符合預期。
實際應用場景
| 場景 | 為什麼需要 exclude_unset / exclude_defaults |
|---|---|
| 行動裝置 API | 手機網路頻寬有限,回傳過多欄位會增加延遲。使用 exclude_unset 可只傳送真正變更的資料。 |
| 多語系內容 | 文字欄位如 title_en, title_zh 若未提供則不回傳,減少冗餘。 |
| 資料分析平台 | 大量記錄的 status 預設為 0,使用 exclude_defaults 可大幅縮減 CSV/JSON 匯出檔案大小。 |
| 第三方 Webhook | Webhook 訊息格式必須嚴格符合對方規範,未提供的欄位必須省略,避免驗證失敗。 |
| 微服務間通訊 | 內部服務間傳遞的 DTO(Data Transfer Object)常包含許多可選欄位,使用 exclude_unset 可讓訊息更精簡,提高訊息佇列吞吐量。 |
總結
response_model_exclude_unset只回傳 實際被設定 的欄位,適合 部分更新、減少大型模型傳輸。exclude_defaults會排除 等於預設值 的欄位,適合 大量預設值 的情境,能顯著節省流量。- 兩者可以 同時使用,配合
exclude_none、response_model_include/exclude,達到最精細的回傳控制。 - 正確掌握這些參數能讓 API 更輕量、更安全,同時提升前端與其他服務的使用體驗。
透過本文提供的概念說明與實作範例,你應該已經能在自己的 FastAPI 專案中靈活運用 exclude_unset 與 exclude_defaults,在不犧牲可讀性的前提下,讓 API 回傳更貼近業務需求。祝開發順利,持續寫出高品質的 FastAPI 服務!