本文 AI 產出,尚未審核

FastAPI 教學 – Pydantic 模型(Request / Response Models)

主題:Optional 與 Union 型別


簡介

FastAPI 中,所有的請求與回應資料都是透過 Pydantic 模型來驗證與序列化的。
當 API 需要接受「可選」參數或「多型」資料結構時,OptionalUnion 兩個型別就顯得格外重要。

  • Optional 讓欄位可以接受 None,同時保留型別檢查。
  • Union 則允許同一個欄位接受多種不同型別(例如 strint),讓 API 能更彈性地處理多變的輸入/輸出。

掌握這兩個概念,不僅可以寫出 更友善的 API,也能避免因型別不符而產生的 400 錯誤,提升使用者體驗與程式碼可維護性。


核心概念

1. 為什麼要使用 Optional

在 Pydantic 中,欄位預設是 必填 的。若某個欄位在請求中可能不存在,必須使用 Optional(或在欄位後加上預設值)告訴 Pydantic「這個欄位可以缺省」。

from typing import Optional
from pydantic import BaseModel

class UserCreate(BaseModel):
    username: str                # 必填
    email: Optional[str] = None  # 可選,若未提供則為 None
  • Optional[str] 等價於 Union[str, None]
  • 若同時設定預設值(如上例的 = None),FastAPI 會在 OpenAPI 文件中把該欄位標記為 非必填

2. Union 的基本用法

Union 允許一個欄位接受多種型別。例如,某個 API 允許使用者以 ID(int)名稱(str) 來查詢資源:

from typing import Union
from pydantic import BaseModel

class ItemLookup(BaseModel):
    identifier: Union[int, str]   # 既可以是數字也可以是字串

FastAPI 會自動根據傳入的資料型別做驗證,若傳入的值既不是 int 也不是 str,就會回傳 422 錯誤。

3. Optional[Union[...]] 的組合

有時候欄位既可以缺省,又可以接受多種型別,這時就需要 Optional[Union[...]]

from typing import Optional, Union

class SearchParams(BaseModel):
    q: Optional[Union[str, int]] = None   # 可不提供;若提供則可為字串或整數

4. 與 LiteralEnum 結合的進階寫法

若想限制 Union 中的字串只能是特定值,可以使用 Literal(Python 3.8+)或 Enum

from typing import Union, Literal
from pydantic import BaseModel

class FilterParams(BaseModel):
    sort_by: Union[Literal["name"], Literal["date"], None] = None

這樣在 Swagger UI 中會直接顯示下拉選單,使用者只能選擇 "name""date" 或留空。

5. 在 Response Model 中使用 Union

回應模型也可以使用 Union,讓 API 根據不同情況返回不同結構:

from typing import Union
from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

class SuccessResponse(BaseModel):
    data: dict
    message: str = "OK"

class ErrorResponse(BaseModel):
    error: str
    code: int

@app.get("/items/{item_id}", response_model=Union[SuccessResponse, ErrorResponse])
async def read_item(item_id: int):
    if item_id == 0:
        return ErrorResponse(error="Item not found", code=404)
    return SuccessResponse(data={"id": item_id, "name": "Sample"})

FastAPI 會根據實例的類型自動產生正確的 OpenAPI schema,讓前端開發者清楚知道可能的回傳格式。


程式碼範例

以下示範 5 個實用範例,從最基礎到較進階的使用情境,說明 OptionalUnion 的結合方式。

範例 1:簡單的 Optional 欄位

from fastapi import FastAPI
from pydantic import BaseModel
from typing import Optional

app = FastAPI()

class RegisterForm(BaseModel):
    username: str
    password: str
    nickname: Optional[str] = None   # 使用者可以不提供暱稱

@app.post("/register")
async def register(form: RegisterForm):
    # 若 nickname 為 None,使用預設值
    nickname = form.nickname or form.username
    return {"user": form.username, "nickname": nickname}

重點nickname 若未傳入,form.nickname 會是 None,不會拋出驗證錯誤。

範例 2:Union 讓欄位接受多種型別

from fastapi import FastAPI, Query
from typing import Union

app = FastAPI()

@app.get("/search")
async def search(q: Union[int, str] = Query(..., description="可以是數字 ID 或文字關鍵字")):
    if isinstance(q, int):
        return {"type": "id", "value": q}
    return {"type": "keyword", "value": q}

說明Query(...) 表示此參數為必填。FastAPI 會根據傳入的型別自動轉換並驗證。

範例 3:Optional + Union 的組合

from fastapi import FastAPI
from pydantic import BaseModel
from typing import Optional, Union

app = FastAPI()

class Filter(BaseModel):
    status: Optional[Union[int, str]] = None   # 允許不傳,或傳 0/1 或 "active"/"inactive"

@app.post("/filter")
async def apply_filter(filter: Filter):
    return {"received": filter.status}

實務意義:前端 UI 可能提供下拉選單(字串)或直接輸入代碼(數字),兩者皆可接受。

範例 4:Response Model 使用 Union

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from typing import Union

app = FastAPI()

class ItemOut(BaseModel):
    id: int
    name: str

class NotFoundOut(BaseModel):
    detail: str

@app.get("/items/{item_id}", response_model=Union[ItemOut, NotFoundOut])
async def get_item(item_id: int):
    if item_id == 42:
        return ItemOut(id=42, name="The Answer")
    raise HTTPException(status_code=404, detail="Item not found")

提示:即使拋出 HTTPException,FastAPI 仍會根據 response_model 產生正確的 schema;若想在成功回傳時提供不同結構,只要回傳對應的 Pydantic 實例即可。

範例 5:結合 Enum、Literal 與 Union

from fastapi import FastAPI, Query
from pydantic import BaseModel
from typing import Union, Literal
from enum import Enum

app = FastAPI()

class Role(str, Enum):
    admin = "admin"
    user = "user"
    guest = "guest"

class AccessRequest(BaseModel):
    role: Union[Role, Literal["superuser"], None] = None   # 允許 Role、"superuser" 或不提供

@app.get("/access")
async def check_access(req: AccessRequest = Query(...)):
    if req.role is None:
        return {"access": "anonymous"}
    if req.role == "superuser":
        return {"access": "full"}
    return {"access": req.role}

關鍵Literal["superuser"] 為單一字串常量,與 Enum 結合可以給予 API 更細緻的控制。


常見陷阱與最佳實踐

陷阱 說明 解決方案
忘記給 Optional 欄位設定預設值 只寫 email: Optional[str],若請求缺少 email,Pydantic 仍會拋出錯誤。 必須寫成 email: Optional[str] = None,或使用 Field(default=None)
使用 Union 時順序不當 Union[int, str]Union[str, int] 行為相同,但若加入 NoneUnion[int, None] 會被解讀為 Optional[int],而 Union[None, int] 也同理。 為避免混淆,建議使用 Optional[int] 取代 Union[int, None]
Swagger UI 中顯示不正確的類型 Union 包含複雜模型時,OpenAPI 可能只顯示第一個子型別。 使用 typing.Annotatedpydantic.Field(..., discriminator="type") 來提供 discriminated unions。
回傳 Union 時忘記設定 response_model 若不指定 response_model,FastAPI 仍會回傳 JSON,但文件缺乏說明。 明確在路由裝飾器中寫 response_model=Union[...,...],讓文件自動生成。
過度使用 Union 把太多型別混在一起會讓驗證變慢、文件難以閱讀。 只在 真的需要多型別 時使用,盡量設計明確的 API 介面。

最佳實踐

  1. 預設值 + Optional:所有可選欄位都要明確給 = NoneField(default=None)
  2. 盡量使用 Literal/Enum:取代自由字串的 Union,提升文件可讀性與前端選項的自動生成。
  3. 分離 Request/Response Model:即使結構相似,也建議分別建立,避免因 Optional 設定不當導致回傳資料不一致。
  4. 使用 discriminated union(Python 3.10+)來描述「同一欄位根據 type 欄位切換不同子模型」的情境。

實際應用場景

  1. 搜尋 API
    用戶可以以 關鍵字 (str)商品編號 (int) 搜尋,Union[str, int] 讓前端只需要一個輸入框即可支援兩種搜尋方式。

  2. 使用者設定
    某些設定項目是可選的,例如 通知頻率dailyweeklyNone),使用 Optional[Literal["daily","weekly"]] 讓 UI 自動產生下拉選單,同時允許使用者不設定。

  3. 多語系回傳
    回傳的文字可能是 純文字物件(包含語言代碼與文字),使用 Union[str, Dict[str, str]] 讓 API 同時支援簡易模式與完整模式。

  4. 錯誤回傳
    不同的錯誤類型需要不同的回傳結構,例如 驗證錯誤(包含欄位列表)與 業務錯誤(只返回訊息),使用 Union[ValidationErrorOut, BusinessErrorOut] 讓前端可以根據 type 欄位做不同的處理。

  5. 動態表單
    當表單欄位的類型會根據前端選擇改變(例如「電話」欄位可以是 國際號碼 (str)本地號碼 (int)),Optional[Union[str, int]] 可讓後端靈活接受。


總結

  • Optional 用於 允許缺省None)的欄位;配合預設值即可在 OpenAPI 中標示為非必填。
  • Union 讓單一欄位接受 多種型別,在 API 必須支援多樣輸入或多種回傳格式時非常有用。
  • Optional[Union[...]] 是最常見的組合,適用於「欄位可不提供,若提供則可為多種型別」的情境。
  • 結合 LiteralEnumAnnotated,可以產生更清晰、使用者友善的文件與 UI。
  • 注意 預設值、型別順序、文件生成 等常見陷阱,遵循 最佳實踐,即可寫出既彈性又穩定的 FastAPI 端點。

掌握了這些技巧,你就能在 FastAPI + Pydantic 的開發中,靈活處理各種可選與多型別的需求,讓 API 更具可擴充性與可維護性。祝開發順利 🚀