FastAPI 教學 – Pydantic 模型(Request / Response Models)
主題:Optional 與 Union 型別
簡介
在 FastAPI 中,所有的請求與回應資料都是透過 Pydantic 模型來驗證與序列化的。
當 API 需要接受「可選」參數或「多型」資料結構時,Optional 與 Union 兩個型別就顯得格外重要。
- Optional 讓欄位可以接受
None,同時保留型別檢查。 - Union 則允許同一個欄位接受多種不同型別(例如
str或int),讓 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. 與 Literal、Enum 結合的進階寫法
若想限制 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 個實用範例,從最基礎到較進階的使用情境,說明 Optional 與 Union 的結合方式。
範例 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] 行為相同,但若加入 None,Union[int, None] 會被解讀為 Optional[int],而 Union[None, int] 也同理。 |
為避免混淆,建議使用 Optional[int] 取代 Union[int, None]。 |
| Swagger UI 中顯示不正確的類型 | 當 Union 包含複雜模型時,OpenAPI 可能只顯示第一個子型別。 |
使用 typing.Annotated 或 pydantic.Field(..., discriminator="type") 來提供 discriminated unions。 |
回傳 Union 時忘記設定 response_model |
若不指定 response_model,FastAPI 仍會回傳 JSON,但文件缺乏說明。 |
明確在路由裝飾器中寫 response_model=Union[...,...],讓文件自動生成。 |
過度使用 Union |
把太多型別混在一起會讓驗證變慢、文件難以閱讀。 | 只在 真的需要多型別 時使用,盡量設計明確的 API 介面。 |
最佳實踐:
- 預設值 + Optional:所有可選欄位都要明確給
= None或Field(default=None)。 - 盡量使用
Literal/Enum:取代自由字串的Union,提升文件可讀性與前端選項的自動生成。 - 分離 Request/Response Model:即使結構相似,也建議分別建立,避免因
Optional設定不當導致回傳資料不一致。 - 使用 discriminated union(Python 3.10+)來描述「同一欄位根據
type欄位切換不同子模型」的情境。
實際應用場景
搜尋 API
用戶可以以 關鍵字 (str) 或 商品編號 (int) 搜尋,Union[str, int]讓前端只需要一個輸入框即可支援兩種搜尋方式。使用者設定
某些設定項目是可選的,例如 通知頻率(daily、weekly、None),使用Optional[Literal["daily","weekly"]]讓 UI 自動產生下拉選單,同時允許使用者不設定。多語系回傳
回傳的文字可能是 純文字 或 物件(包含語言代碼與文字),使用Union[str, Dict[str, str]]讓 API 同時支援簡易模式與完整模式。錯誤回傳
不同的錯誤類型需要不同的回傳結構,例如 驗證錯誤(包含欄位列表)與 業務錯誤(只返回訊息),使用Union[ValidationErrorOut, BusinessErrorOut]讓前端可以根據type欄位做不同的處理。動態表單
當表單欄位的類型會根據前端選擇改變(例如「電話」欄位可以是 國際號碼 (str) 或 本地號碼 (int)),Optional[Union[str, int]]可讓後端靈活接受。
總結
Optional用於 允許缺省(None)的欄位;配合預設值即可在 OpenAPI 中標示為非必填。Union讓單一欄位接受 多種型別,在 API 必須支援多樣輸入或多種回傳格式時非常有用。Optional[Union[...]]是最常見的組合,適用於「欄位可不提供,若提供則可為多種型別」的情境。- 結合
Literal、Enum、Annotated,可以產生更清晰、使用者友善的文件與 UI。 - 注意 預設值、型別順序、文件生成 等常見陷阱,遵循 最佳實踐,即可寫出既彈性又穩定的 FastAPI 端點。
掌握了這些技巧,你就能在 FastAPI + Pydantic 的開發中,靈活處理各種可選與多型別的需求,讓 API 更具可擴充性與可維護性。祝開發順利 🚀