本文 AI 產出,尚未審核

FastAPI 教學:response_model_exclude_unsetexclude_defaults 的使用


簡介

在開發 API 時,回傳的資料結構往往需要與前端或第三方服務保持一致。
FastAPI 透過 Pydantic 的 BaseModel 讓我們可以在 response_model 中宣告回傳格式,同時提供了 exclude_unsetexclude_defaults 等參數,協助開發者自動過濾掉不必要的欄位,減少網路傳輸量、提升效能,也能避免洩漏敏感資訊。

本篇文章將深入說明 response_model_exclude_unsetexclude_defaults 兩個功能的差異、使用時機,以及在實務專案中如何正確運用它們。文章適合 FastAPI 初學者 以及 想要優化回傳結果的中階開發者,透過具體範例,你將能快速上手並避免常見陷阱。


核心概念

1. Pydantic BaseModeldict() 轉換

FastAPI 依賴 Pydantic 來驗證與序列化資料。BaseModel 提供的 dict() 方法接受多個參數,常見的有:

參數 說明
exclude_unset 只保留 已被設定(即非預設值)的欄位。未提供的欄位會被排除。
exclude_defaults 只保留 值不等於欄位預設值 的欄位。即使欄位被明確指定為預設值,也會被排除。
exclude_none 排除值為 None 的欄位。

重點response_model_exclude_unsetexclude_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 只傳 nameprice,回傳結果會是:
{
  "name": "Apple",
  "price": 2.5
}
  • descriptiontax 因為 未被設定,被 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_activerole 因為等於預設值,被 exclude_defaults 自動過濾。

範例 3:同時使用 exclude_unsetexclude_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 再度過濾掉,最終回傳只剩下原始的 nameprice

範例 4:結合 exclude_noneexclude_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"
}
  • descriptionNone,被 exclude_none=True 排除;
  • tags 為預設空陣列,因為 已被設定(即使是空陣列),仍會保留,若想排除空陣列可自行在 dict() 時處理。

範例 5:自訂 response_model_includeresponse_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 明確指定只回傳 nameprice
  • 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 文件中明確說明「哪些欄位可能會被省略」。
過度依賴排除機制 為了減少傳輸量而大量排除欄位,可能讓前端必須自行補足缺失的預設值,增加錯誤機率。 僅在確定前端能接受「缺失欄位」的情況下使用;若前端需要完整結構,建議保留欄位或在前端自行補值。

最佳實踐

  1. 先定義清晰的 API 規範:在 OpenAPI 文件中註明哪些欄位是「可選」且可能被省略。
  2. 根據業務需求選擇排除策略
    • exclude_unset → 只在「部分更新」或「大模型」時使用。
    • exclude_defaults → 在「大量預設值」且 不需要 前端知道的情況下使用。
  3. 結合 exclude_none:避免回傳 null,提升前端資料處理的便利性。
  4. 使用自訂 dict():在極端情況下,直接在路由內部呼叫 model.dict(exclude_unset=True, exclude_defaults=True),再回傳 JSONResponse,可獲得最細緻的控制。
  5. 測試所有組合:使用 pytesthttpx 測試不同請求情形,確保回傳結果符合預期。

實際應用場景

場景 為什麼需要 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_noneresponse_model_include/exclude,達到最精細的回傳控制。
  • 正確掌握這些參數能讓 API 更輕量更安全,同時提升前端與其他服務的使用體驗。

透過本文提供的概念說明與實作範例,你應該已經能在自己的 FastAPI 專案中靈活運用 exclude_unsetexclude_defaults,在不犧牲可讀性的前提下,讓 API 回傳更貼近業務需求。祝開發順利,持續寫出高品質的 FastAPI 服務!