本文 AI 產出,尚未審核
FastAPI 課程 – 資料驗證與轉換(Validation & Serialization)
主題:Pydantic Validators
簡介
在 FastAPI 中,資料模型的驗證與序列化全部交給 Pydantic 處理。
Pydantic 不僅能自動根據型別提示產生 JSON Schema,還提供了 validator 機制,讓開發者可以在模型被建立前、或欄位值變更時,加入自訂的檢查與轉換邏輯。
掌握 validator 的寫法,意味著你可以:
- 保證資料完整性:避免不合法的輸入流入業務邏輯。
- 統一資料格式:自動把字串、時間、金額等轉成一致的型別。
- 減少重複程式碼:把驗證邏輯集中在模型內,而不是散落在每個路由函式。
本篇文章將從概念說明、實作範例、常見陷阱與最佳實踐,帶你一步一步在 FastAPI 專案中運用 Pydantic validators。
核心概念
1. 基本的 @validator 用法
Pydantic 透過 @validator 裝飾器,讓你可以在模型建立時對特定欄位執行自訂函式。
最簡單的形式是:
from pydantic import BaseModel, validator
class User(BaseModel):
username: str
age: int
@validator('age')
def age_must_be_positive(cls, v):
"""年齡必須是正整數"""
if v <= 0:
raise ValueError('age 必須大於 0')
return v
cls為模型本身(可省略),v為欄位的原始值。- 若拋出
ValueError、TypeError或AssertionError,Pydantic 會把錯誤收集起來,回傳給 FastAPI 的 422 回應。
2. 多欄位交叉驗證 (@root_validator)
有時候驗證需要同時參考多個欄位,例如 開始日期 必須早於 結束日期。此時使用 @root_validator:
from datetime import datetime
from pydantic import BaseModel, root_validator
class Event(BaseModel):
name: str
start_at: datetime
end_at: datetime
@root_validator
def check_dates(cls, values):
"""確保結束時間晚於開始時間"""
start, end = values.get('start_at'), values.get('end_at')
if start and end and start >= end:
raise ValueError('end_at 必須晚於 start_at')
return values
values 為整個模型的字典,回傳後會繼續進行後續的欄位驗證。
3. 前置與後置驗證 (pre=True、always=True)
- pre=True:在其他欄位驗證之前先執行,常用於 資料清理(例如去除空白、轉換型別)。
- always=True:即使欄位在輸入資料中缺失,也會執行 validator,適合設定 預設值 或 必填檢查。
class Product(BaseModel):
name: str
price: float
tags: list[str] = []
@validator('name', pre=True, always=True)
def strip_name(cls, v):
"""去除名稱前後空白"""
return v.strip() if isinstance(v, str) else v
@validator('price')
def price_must_be_positive(cls, v):
if v <= 0:
raise ValueError('price 必須大於 0')
return round(v, 2) # 兩位小數
4. 多欄位共享同一個 validator
如果多個欄位需要相同的驗證規則,只要在 @validator 裡列出欄位名稱的 tuple 即可:
class LoginForm(BaseModel):
email: str
phone: str
@validator('email', 'phone')
def not_empty(cls, v, field):
"""email 與 phone 不能為空字串"""
if not v or not v.strip():
raise ValueError(f'{field.name} 不能為空')
return v
field 參數會傳入欄位資訊,讓錯誤訊息更具體。
5. 使用外部函式或類別方法
當驗證邏輯較為複雜,建議抽離成獨立函式或類別方法,保持模型簡潔:
def is_valid_password(pw: str) -> bool:
"""密碼必須至少 8 位,且包含大小寫與數字"""
import re
return bool(re.fullmatch(r'(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}', pw))
class RegisterForm(BaseModel):
username: str
password: str
@validator('password')
def check_password_strength(cls, v):
if not is_valid_password(v):
raise ValueError('密碼強度不足')
return v
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方案 |
|---|---|---|
| 忘記返回值 | validator 必須回傳處理後的值,否則欄位會變成 None。 |
確保每個 validator 都有 return v(或轉換後的值)。 |
過度使用 pre=True |
會在資料尚未被 Pydantic 解析前執行,可能導致型別錯誤。 | 僅在需要「原始字串清理」時使用 pre=True。 |
在 @root_validator 中拋出非 ValueError |
只接受 ValueError、TypeError、AssertionError,其他例外會變成 500 錯誤。 |
使用 raise ValueError(...) 進行驗證失敗。 |
| 重複驗證 | 多個 validator 針對相同欄位執行相同檢查,增加維護成本。 | 合併驗證邏輯或抽成共用函式。 |
忘記 always=True |
欄位缺失時 validator 不會執行,導致預設值未被正確處理。 | 若需要在缺失時也跑驗證,加入 always=True。 |
最佳實踐:
- 保持 validator 簡潔:每個 validator 只做一件事,讓錯誤訊息更明確。
- 使用自訂例外類別(可選):若想在 API 回傳更結構化的錯誤,可自訂
PydanticErrorMixin。 - 將驗證邏輯抽成工具函式:讓模型保持乾淨,且方便單元測試。
- 搭配型別提示:利用
typing(如Literal、conint)減少手寫 validator 的需求。
實際應用場景
1. 電子商務 – 商品價格與折扣
from pydantic import BaseModel, validator, condecimal
class Item(BaseModel):
name: str
price: condecimal(gt=0, max_digits=10, decimal_places=2) # 正數、兩位小數
discount: condecimal(ge=0, le=1, max_digits=3, decimal_places=2) = 0
@validator('price')
def apply_discount(cls, v, values):
"""若有折扣,計算實際售價"""
discount = values.get('discount', 0)
if discount:
v = v * (1 - discount)
return round(v, 2)
- 場景說明:使用
condecimal限制價格與折扣範圍,apply_discount自動把折扣套用到價格,避免在服務層重複計算。
2. 金融系統 – 日期與時間校驗
from datetime import date, datetime, time
from pydantic import BaseModel, root_validator
class Transaction(BaseModel):
txn_id: str
amount: float
txn_date: date
txn_time: time
@root_validator
def combine_datetime(cls, values):
"""將日期與時間合併成完整的 datetime,供後續使用"""
d, t = values.get('txn_date'), values.get('txn_time')
if d and t:
values['timestamp'] = datetime.combine(d, t)
return values
- 場景說明:前端可能分別送出
date與time,透過root_validator合併成timestamp,減少資料庫層的轉換成本。
3. 會員系統 – 密碼強度與 Email 格式
import re
from pydantic import BaseModel, validator, EmailStr
class RegisterDTO(BaseModel):
email: EmailStr
password: str
@validator('password')
def password_policy(cls, v):
pattern = r'(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}'
if not re.fullmatch(pattern, v):
raise ValueError('密碼必須至少 8 位,且包含大小寫與數字')
return v
- 場景說明:利用 Pydantic 內建的
EmailStr檢查 Email 格式,額外自訂密碼策略,全部在模型層完成。
總結
- Pydantic validators 是 FastAPI 中保證資料正確性的核心工具。
- 透過
@validator、@root_validator、pre=True、always=True等參數,你可以靈活地處理 單欄位、跨欄位、前置清理 與 後置轉換。 - 避免常見陷阱(忘記回傳值、過度使用
pre、不當的例外類型),遵守 單一職責、抽離共用函式 的最佳實踐,能讓模型既可讀又可維護。 - 在電商、金融、會員等實務場景中,適當運用 validators 能大幅降低業務層的防呆邏輯,提升 API 的一致性與安全性。
掌握了這些技巧後,你就能在 FastAPI 專案中寫出乾淨、可靠且易於擴充的資料驗證程式,為整體系統品質奠定堅實基礎。祝開發順利 🚀