FastAPI – Pydantic 模型(Request / Response Models)
欄位型別註記與預設值
簡介
在 FastAPI 中,所有的請求與回應資料都是透過 Pydantic 模型來定義與驗證的。
欄位的 型別註記(type annotation) 與 預設值(default value) 不只是語法糖,它們直接決定了:
- 自動產生的 OpenAPI 文件(讓前端與第三方系統一目了然)。
- 輸入資料的驗證邏輯(錯誤回傳、型別轉換、欄位必填與否)。
- 回應資料的序列化(確保回傳 JSON 的結構與欄位型別正確)。
對於剛接觸 FastAPI 的開發者來說,正確使用型別註記與預設值是避免 API 失敗與維護成本飆升的關鍵。本篇文章將從概念說明、實作範例、常見陷阱與最佳實踐,一步步帶你掌握 Pydantic 欄位型別與預設值 的使用技巧,並提供實務應用情境,讓你能在專案中即刻上手。
核心概念
1. 型別註記的基本原則
Pydantic 依賴 Python 的 型別提示(type hints)來決定每個欄位的資料型別。常見的型別包括:
| 型別 | 說明 | 範例 |
|---|---|---|
int |
整數 | age: int |
float |
浮點數 | price: float |
str |
字串 | name: str |
bool |
布林值 | is_active: bool |
datetime |
日期時間 | created_at: datetime |
list[T] / Tuple[T, ...] |
陣列或元組 | tags: List[str] |
dict[str, T] |
字典 | metadata: Dict[str, Any] |
Enum |
列舉型別 | status: StatusEnum |
Optional[T] |
可為 None(即非必填) |
nickname: Optional[str] = None |
重要:Pydantic 會在模型實例化時自動 轉換(coerce)相容的資料,例如把字串
"123"轉成int 123,或把"2023-01-01"轉成datetime。若轉換失敗,會拋出ValidationError,FastAPI 會自動回傳 422 錯誤給前端。
2. 預設值的作用
預設值有兩大功能:
- 標示欄位非必填:若欄位有預設值,即使請求中未提供該欄位,模型仍會成功建立,並使用預設值。
- 提供業務邏輯的預設行為:例如
page: int = 1、page_size: int = 20,讓分頁 API 在未指定時仍能正常運作。
2.1 必填 vs. 非必填
| 定義方式 | 必填? | 說明 |
|---|---|---|
field: int |
✅ 必填 | 未提供會產生 422 |
field: int = 0 |
❌ 非必填 | 未提供會使用 0 |
field: Optional[int] = None |
❌ 非必填 | 未提供會是 None |
小技巧:若僅想讓欄位允許
None,但仍想保留型別檢查,使用Optional[T],而不是直接T = None(後者會讓型別變成Any)。
3. 進階型別與驗證
3.1 conint, confloat, constr
Pydantic 提供 限制型別(constrained types)讓你在宣告時就加入驗證規則:
from pydantic import BaseModel, conint, condecimal, constr
class ProductCreate(BaseModel):
# 價格必須大於 0,且最多兩位小數
price: condecimal(gt=0, max_digits=10, decimal_places=2)
# 庫存只能是非負整數
stock: conint(ge=0)
# 商品名稱長度 1~100
name: constr(min_length=1, max_length=100)
3.2 Field 與額外資訊
Field 允許你設定 描述、別名、範例值,同時支援 JSON Schema 的額外屬性,對於自動產生的 API 文件非常有幫助:
from pydantic import BaseModel, Field
from typing import List
class OrderQuery(BaseModel):
# 別名可讓前端使用 snake_case 或 camelCase
page: int = Field(1, ge=1, description="頁碼,從 1 開始")
page_size: int = Field(20, ge=1, le=100, description="每頁顯示筆數")
# 使用別名
sort_by: str = Field("created_at", alias="sortBy", description="排序欄位")
# 範例值會出現在 Swagger UI
tags: List[str] = Field(default_factory=list, example=["electronics", "sale"])
4. 範例:從 Request 到 Response 的完整流程
以下示範一個「建立使用者」的 API,涵蓋 請求模型、回應模型、預設值 與 型別註記:
# file: main.py
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, EmailStr, Field
from typing import Optional
from datetime import datetime
app = FastAPI(title="User API 範例")
class UserCreateRequest(BaseModel):
"""建立使用者的請求模型"""
username: str = Field(..., min_length=3, max_length=20, description="使用者名稱")
email: EmailStr = Field(..., description="有效的 Email 地址")
age: Optional[int] = Field(None, ge=0, le=150, description="年齡,若未提供則為 None")
is_active: bool = Field(True, description="是否啟用帳號,預設為 True")
class UserResponse(BaseModel):
"""回傳給前端的使用者資料模型"""
id: int = Field(..., description="系統自動產生的使用者 ID")
username: str
email: EmailStr
age: Optional[int] = None
is_active: bool
created_at: datetime = Field(default_factory=datetime.utcnow)
# 模擬資料庫
fake_db = {}
next_id = 1
@app.post("/users", response_model=UserResponse, status_code=201)
def create_user(payload: UserCreateRequest):
global next_id
# 簡易檢查:Email 必須唯一
if any(u["email"] == payload.email for u in fake_db.values()):
raise HTTPException(status_code=400, detail="Email already exists")
user = payload.dict()
user["id"] = next_id
user["created_at"] = datetime.utcnow()
fake_db[next_id] = user
next_id += 1
return user
說明
UserCreateRequest中的is_active設定了預設值True,因此即使前端不傳此欄位,資料仍會被視為啟用。age使用Optional[int] = None,表示此欄位可省略或傳null。UserResponse透過default_factory=datetime.utcnow為created_at自動產生時間戳。response_model=UserResponse讓 FastAPI 在回傳前自動過濾、序列化,並產生正確的 OpenAPI 文件。
常見陷阱與最佳實踐
| 陷阱 | 為什麼會發生 | 解決方式 |
|---|---|---|
預設值與 Optional 搭配錯誤 |
直接寫 field: Optional[int] = 0 會把型別變成 int(因為預設值非 None),導致驗證不符合預期。 |
若想允許 None,必須將預設值設為 None,或使用 `field: int |
| 使用可變物件作為預設值 | field: List[int] = [] 會在每次實例化時共用同一個列表,導致資料互相污染。 |
使用 default_factory=list 或 Field(default_factory=list)。 |
別名(alias)與 populate_by_name 忽略 |
當前端傳送 camelCase 時,模型若未開啟 populate_by_name,會拋出 422。 |
在 BaseModel.Config 中設定 allow_population_by_field_name = True,或在 FastAPI 路由上使用 response_model_by_alias=False。 |
過度依賴 Any |
把欄位型別寫成 Any 會失去驗證與文件生成的好處。 |
盡量使用具體型別或 Union,必要時才使用 Any。 |
忽略 example / description |
Swagger UI 沒有說明,前端開發者不易理解。 | 在 Field 中加入 description、example,提升可讀性。 |
最佳實踐清單
- 始終使用型別註記:即使是
str也要明確寫username: str,讓 Pydantic 能正確產生 JSON Schema。 - 預設值即代表非必填:若欄位必填,使用
...(Ellipsis)或不提供預設值。 - 避免可變物件作為預設值:使用
default_factory。 - 善用
Field:加入description、example、ge/le等限制,讓文件自動說明。 - 測試驗證邏輯:使用
client.post(..., json=payload)測試各種缺失與錯誤情況,確保 422 錯誤訊息清晰。
實際應用場景
1. 分頁 API
class PaginationParams(BaseModel):
page: int = Field(1, ge=1, description="第幾頁")
size: int = Field(20, ge=1, le=100, description="每頁筆數")
- 好處:前端不傳參數時會自動使用預設值,避免必填錯誤。
2. 多語系文字欄位
class ProductCreate(BaseModel):
name: dict = Field(..., description="多語系商品名稱,例如 {'en': 'Apple', 'zh': '蘋果'}")
price: float = Field(..., gt=0)
- 說明:使用
dict作為欄位型別,並在description中說明結構,讓前端明白 JSON 結構。
3. 啟用/停用功能的切換
class FeatureToggle(BaseModel):
feature_name: str
enabled: bool = Field(True, description="預設為啟用")
- 情境:管理平台可一次更新多個功能的狀態,若未傳
enabled,系統會保留原本的狀態或使用預設值。
4. 日期範圍查詢
from datetime import date
class DateRangeQuery(BaseModel):
start_date: date = Field(..., description="查詢起始日期")
end_date: date = Field(..., description="查詢結束日期")
include_weekends: bool = Field(False, description="是否包含週末,預設不包含")
- 優點:
date型別會自動從字串轉換,若格式錯誤會直接返回 422,減少手動驗證程式碼。
總結
- 型別註記是 Pydantic 驗證與 FastAPI 自動產生 OpenAPI 文件的根基,務必正確、完整。
- 預設值不只是「沒有傳就用什麼」的設定,它同時決定欄位是否為必填,並能提供業務邏輯的預設行為。
- 使用
Field、default_factory、限制型別(如conint、constr)可以在模型層級完成大部分驗證,讓路由函式保持簡潔。 - 注意 可變預設值、別名、Optional 的細節,避免常見的驗證失誤與資料污染。
- 在實務開發中,將 請求模型、回應模型、分頁/篩選參數 都抽成獨立的 Pydantic 類別,不僅提升程式可讀性,也讓 API 文件自動保持同步。
掌握了欄位型別註記與預設值的正確寫法,你的 FastAPI 專案將能在 驗證安全、文件完整、開發效率 三方面同時受益。祝你開發順利,打造出高品質的 RESTful API!