本文 AI 產出,尚未審核
FastAPI 課程 – Pydantic v2 的 model_validator() 與 field_validator()
簡介
在 FastAPI 中,Pydantic 是負責資料驗證與序列化的核心工具。從 Pydantic v2 開始,舊有的 @validator、@root_validator 已被全新機制取代:field_validator() 與 model_validator()。這兩個 decorator 不僅語法更直觀,也支援 同步 / 非同步 的驗證流程,讓開發者能更輕鬆地寫出可讀性高、效能佳的驗證程式碼。
本單元將說明:
- 什麼是
field_validator()與model_validator(),它們的差異與使用時機。 - 在 Request / Response Model 中如何運用這兩個 validator,確保 API 收到的資料符合商業規則。
- 常見的陷阱、最佳實踐以及實務應用情境。
核心概念
1. 為什麼需要新驗證機制?
- 更清晰的作用範圍:
field_validator()只負責單一欄位的前後處理,model_validator()則負責整個模型層級的驗證(類似舊版的root_validator)。 - 支援非同步:在需要呼叫外部 API、資料庫或其他 I/O 操作的驗證時,只要把 validator 定義成
async def即可。 - 型別安全:Pydantic v2 內建 TypedDict 與 Annotated 支援,讓驗證過程更貼合型別提示。
2. field_validator() 基本語法
from pydantic import BaseModel, Field, field_validator
class UserCreate(BaseModel):
username: str = Field(..., min_length=3, max_length=20)
email: str
age: int | None = None
# 只驗證單一欄位
@field_validator('email')
def validate_email(cls, v: str) -> str:
"""檢查 email 是否符合簡易正規表達式"""
if '@' not in v:
raise ValueError('email 必須包含 @')
return v.lower()
重點:
@field_validator的第一個參數是要驗證的欄位名稱(支援 list),回傳值會自動寫回模型。
3. model_validator() 基本語法
from pydantic import BaseModel, model_validator
class Order(BaseModel):
product_id: int
quantity: int
price: float
# 針對整個模型的驗證
@model_validator(mode='after')
def check_total(cls, values):
"""確保總金額不會超過 10,000"""
total = values.quantity * values.price
if total > 10_000:
raise ValueError('訂單金額不能超過 10,000')
return values
mode='before':在欄位轉型之前執行,適合需要 預處理 原始資料的情境。mode='after':在所有欄位都已完成轉型之後執行,適合 跨欄位邏輯(如上例)。
4. 同步 vs 非同步驗證
from pydantic import BaseModel, field_validator, model_validator
import httpx
class PromoCode(BaseModel):
code: str
@field_validator('code')
async def verify_code(cls, v: str) -> str:
"""呼叫外部服務驗證促銷碼是否有效"""
async with httpx.AsyncClient() as client:
resp = await client.get(f'https://api.example.com/promo/{v}')
data = resp.json()
if not data.get('valid'):
raise ValueError('促銷碼無效')
return v
實務提示:在 FastAPI 中,若模型的 validator 為 async,FastAPI 會自動以非同步方式呼叫,無需額外包裝。
5. 多欄位同時驗證(list 版 field_validator)
class Registration(BaseModel):
password: str
confirm_password: str
@field_validator('confirm_password')
def passwords_match(cls, v, values):
"""確認兩次密碼輸入相同"""
if 'password' in values and v != values['password']:
raise ValueError('兩次密碼不一致')
return v
values參數會帶入已驗證過的其他欄位資料(僅在mode='after'時可用)。
6. 結合 FastAPI 的 Request / Response Model
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, field_validator, model_validator
app = FastAPI()
class ItemCreate(BaseModel):
name: str
price: float
discount: float | None = None
@field_validator('price')
def positive_price(cls, v):
if v <= 0:
raise ValueError('price 必須大於 0')
return v
@model_validator(mode='after')
def apply_discount(cls, values):
if values.discount:
if not (0 < values.discount < 1):
raise ValueError('discount 必須在 0~1 之間')
values.price = round(values.price * (1 - values.discount), 2)
return values
@app.post("/items/")
async def create_item(item: ItemCreate):
# 此時 item 已經完成所有驗證與折扣計算
return {"msg": "建立成功", "item": item}
- Response Model 只需要把
ItemCreate用於response_model,Pydantic 會自動套用同樣的驗證與序列化規則。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方式 |
|---|---|---|
忘記指定 mode |
預設是 after,在 before 時想存取已轉型欄位會失敗。 |
明確寫 @model_validator(mode='before') 或 @field_validator(..., mode='before')。 |
在 field_validator 中直接修改其他欄位 |
只能在 model_validator 或 field_validator(..., mode='after') 中取得 values。 |
使用 model_validator 或把驗證邏輯搬到 after 模式。 |
| 非同步 validator 被同步呼叫 | 若在同步函式中使用 async validator,會拋出 RuntimeError。 |
確保 FastAPI 路由是 async,或改寫為同步。 |
| 過度驗證 | 把所有商業規則都塞進模型,導致模型過於龐大、難以維護。 | 把領域規則放在服務層(service layer),僅保留資料完整性於模型。 |
未考慮 None 值 |
field_validator 預設會在 None 時跳過驗證。 |
若需要檢查 None,加上 check_fields=False 或自行處理。 |
最佳實踐:
- 分層驗證:資料型別驗證放在 Pydantic,商業邏輯放在服務層。
- 使用
mode='before'進行預處理,例如字串去除空白、轉換時間格式。 - 盡量保持 validator 簡潔:每個 validator 只做一件事,方便單元測試。
- 加入型別提示:
def validator(cls, v: str) -> str,讓 IDE 能提供即時錯誤提示。 - 測試非同步 validator:使用
pytest-asyncio來驗證 async 行為。
實際應用場景
| 場景 | 為何使用 field_validator / model_validator |
|---|---|
| 使用者註冊 | field_validator 檢查 email、密碼強度;model_validator 確認密碼與確認密碼相符。 |
| 電子商務訂單 | field_validator 驗證商品編號、數量;model_validator 計算總金額、檢查庫存上限。 |
| 第三方 API 整合 | field_validator (async) 呼叫外部服務驗證優惠碼或信用卡號。 |
| 多語系內容 | model_validator(mode='before') 把前端傳入的 locale 轉為標準化代碼,再交給 field_validator 處理文字長度。 |
| 批次匯入 CSV | 先用 model_validator(mode='before') 把字串日期轉為 datetime,再用 field_validator 檢查數值範圍。 |
總結
- Pydantic v2 以
field_validator()與model_validator()取代舊版的@validator、@root_validator,提供 更清晰的驗證層級 與 非同步支援。 - 在 FastAPI 中,將這兩個 validator 融入 Request / Response Model,可以在 API 入口即完成資料完整性與基本商業規則的檢查,減少後端服務層的防禦性程式碼。
- 了解
mode='before'/mode='after'的差異、正確使用values、以及何時採用 同步 vs 非同步,是寫好驗證程式的關鍵。 - 避免過度驗證、保持 validator 單一職責、並配合服務層的業務邏輯,能讓程式碼更易維護、測試更完整。
掌握了這套新驗證機制,你的 FastAPI 專案將能在 資料安全、效能 與 可讀性 上同時提升,為日後的功能擴充與團隊協作奠定堅實基礎。祝開發順利! 🚀