目錄
02-3. 自訂驗證器
⏱️ 閱讀時間: 15 分鐘 🎯 難度: ⭐⭐⭐ (中級)
🤔 一句話解釋
當內建驗證不夠用時,使用自訂驗證器實現任何你想要的驗證邏輯。
🎯 Pydantic v2 驗證器類型
┌─────────────────────────────────────────────┐
│ 驗證器執行順序 │
├─────────────────────────────────────────────┤
│ 1. before validator (原始輸入) │
│ ↓ │
│ 2. 型別轉換/核心驗證 │
│ ↓ │
│ 3. after validator (轉換後的值) │
│ ↓ │
│ 4. model_validator (整個模型) │
└─────────────────────────────────────────────┘🔧 field_validator - 欄位驗證器
基本用法
from pydantic import BaseModel, field_validator
class User(BaseModel):
name: str
age: int
@field_validator('name')
@classmethod
def name_must_not_be_empty(cls, v: str) -> str:
if not v.strip():
raise ValueError('名稱不能為空')
return v.strip()
@field_validator('age')
@classmethod
def age_must_be_positive(cls, v: int) -> int:
if v < 0:
raise ValueError('年齡必須大於 0')
return v
# 測試
user = User(name=" John ", age=25)
print(user.name) # "John" (已去除空白)驗證多個欄位
from pydantic import BaseModel, field_validator
class User(BaseModel):
first_name: str
last_name: str
email: str
# 同時驗證多個欄位
@field_validator('first_name', 'last_name')
@classmethod
def names_not_empty(cls, v: str) -> str:
if not v.strip():
raise ValueError('名稱不能為空')
return v.strip().title() # 首字大寫
@field_validator('email')
@classmethod
def email_lowercase(cls, v: str) -> str:
return v.lower().strip()
# 測試
user = User(
first_name="john",
last_name="DOE",
email="JOHN@EXAMPLE.COM"
)
print(user.first_name) # John
print(user.last_name) # Doe
print(user.email) # john@example.com使用驗證模式
from pydantic import BaseModel, field_validator, ValidationInfo
class Product(BaseModel):
name: str
price: float
discount_price: float = None
# mode='before': 在型別轉換之前執行
@field_validator('price', mode='before')
@classmethod
def parse_price(cls, v):
if isinstance(v, str):
# 移除貨幣符號
v = v.replace('$', '').replace(',', '')
return float(v)
# mode='after': 在型別轉換之後執行(預設)
@field_validator('price', mode='after')
@classmethod
def price_must_be_positive(cls, v: float) -> float:
if v <= 0:
raise ValueError('價格必須大於 0')
return round(v, 2)
# 測試
product = Product(name="iPhone", price="$1,299.99")
print(product.price) # 1299.99存取其他欄位的值
from pydantic import BaseModel, field_validator, ValidationInfo
class DateRange(BaseModel):
start_date: str
end_date: str
@field_validator('end_date')
@classmethod
def end_after_start(cls, v: str, info: ValidationInfo) -> str:
# info.data 包含已驗證的欄位
if 'start_date' in info.data:
if v < info.data['start_date']:
raise ValueError('結束日期必須在開始日期之後')
return v
class PasswordConfirm(BaseModel):
password: str
confirm_password: str
@field_validator('confirm_password')
@classmethod
def passwords_match(cls, v: str, info: ValidationInfo) -> str:
if 'password' in info.data and v != info.data['password']:
raise ValueError('密碼不一致')
return v🏗️ model_validator - 模型驗證器
驗證整個模型
from pydantic import BaseModel, model_validator
class Rectangle(BaseModel):
width: float
height: float
area: float = None
@model_validator(mode='after')
def calculate_area(self):
if self.area is None:
self.area = self.width * self.height
elif self.area != self.width * self.height:
raise ValueError('面積與寬高不符')
return self
# 測試
rect = Rectangle(width=10, height=5)
print(rect.area) # 50.0
rect2 = Rectangle(width=10, height=5, area=50) # ✅ OK
rect3 = Rectangle(width=10, height=5, area=100) # ❌ ValidationErrorbefore 模式 - 處理原始輸入
from pydantic import BaseModel, model_validator
from typing import Any
class FlexibleUser(BaseModel):
name: str
email: str
@model_validator(mode='before')
@classmethod
def handle_string_input(cls, data: Any) -> Any:
# 如果輸入是字串,嘗試解析
if isinstance(data, str):
# 假設格式是 "name:email"
parts = data.split(':')
if len(parts) == 2:
return {'name': parts[0], 'email': parts[1]}
return data
# 測試
user1 = FlexibleUser(name="John", email="john@example.com")
user2 = FlexibleUser.model_validate("John:john@example.com")wrap 模式 - 包裝核心驗證
from pydantic import BaseModel, model_validator, ValidationError
class SafeModel(BaseModel):
value: int
@model_validator(mode='wrap')
@classmethod
def wrap_validation(cls, values, handler):
try:
return handler(values)
except ValidationError as e:
# 可以在這裡處理錯誤
print(f"Validation failed: {e}")
raise🔄 Annotated Validators
使用 Annotated 定義可重用驗證器
from pydantic import BaseModel, AfterValidator, BeforeValidator
from typing import Annotated
# 定義驗證函數
def to_lowercase(v: str) -> str:
return v.lower()
def strip_whitespace(v: str) -> str:
return v.strip()
def must_not_be_empty(v: str) -> str:
if not v:
raise ValueError('不能為空')
return v
# 組合成可重用的型別
CleanString = Annotated[
str,
BeforeValidator(strip_whitespace),
AfterValidator(to_lowercase),
AfterValidator(must_not_be_empty),
]
class User(BaseModel):
username: CleanString
email: CleanString
# 測試
user = User(username=" JOHN ", email=" JOHN@EXAMPLE.COM ")
print(user.username) # john
print(user.email) # john@example.com常用可重用驗證器
from pydantic import BaseModel, AfterValidator, BeforeValidator, Field
from typing import Annotated
import re
# 去除空白
def strip(v: str) -> str:
return v.strip() if isinstance(v, str) else v
# 轉小寫
def lower(v: str) -> str:
return v.lower() if isinstance(v, str) else v
# 轉大寫
def upper(v: str) -> str:
return v.upper() if isinstance(v, str) else v
# 首字大寫
def title(v: str) -> str:
return v.title() if isinstance(v, str) else v
# 非空驗證
def not_empty(v: str) -> str:
if not v or not v.strip():
raise ValueError('不能為空')
return v
# 正規表達式驗證
def matches_pattern(pattern: str):
def validator(v: str) -> str:
if not re.match(pattern, v):
raise ValueError(f'格式不符合: {pattern}')
return v
return validator
# 組合成常用型別
TrimmedString = Annotated[str, BeforeValidator(strip)]
LowerString = Annotated[str, BeforeValidator(strip), AfterValidator(lower)]
UpperString = Annotated[str, BeforeValidator(strip), AfterValidator(upper)]
TitleString = Annotated[str, BeforeValidator(strip), AfterValidator(title)]
NonEmptyString = Annotated[str, BeforeValidator(strip), AfterValidator(not_empty)]
# 使用
class Person(BaseModel):
first_name: TitleString
last_name: TitleString
nickname: TrimmedString = ""
person = Person(first_name=" john ", last_name=" DOE ")
print(person.first_name) # John
print(person.last_name) # Doe🎨 實戰範例
信用卡驗證
from pydantic import BaseModel, field_validator, model_validator
from datetime import date
import re
class CreditCard(BaseModel):
card_number: str
expiry_month: int
expiry_year: int
cvv: str
holder_name: str
@field_validator('card_number')
@classmethod
def validate_card_number(cls, v: str) -> str:
# 移除空格和破折號
v = re.sub(r'[\s-]', '', v)
# 檢查是否全為數字
if not v.isdigit():
raise ValueError('卡號只能包含數字')
# 檢查長度(13-19 位)
if not 13 <= len(v) <= 19:
raise ValueError('卡號長度不正確')
# Luhn 演算法驗證
def luhn_check(card_num):
digits = [int(d) for d in card_num]
odd_digits = digits[-1::-2]
even_digits = digits[-2::-2]
checksum = sum(odd_digits)
for d in even_digits:
checksum += sum(divmod(d * 2, 10))
return checksum % 10 == 0
if not luhn_check(v):
raise ValueError('無效的卡號')
return v
@field_validator('expiry_month')
@classmethod
def validate_month(cls, v: int) -> int:
if not 1 <= v <= 12:
raise ValueError('月份必須在 1-12 之間')
return v
@field_validator('expiry_year')
@classmethod
def validate_year(cls, v: int) -> int:
current_year = date.today().year
if v < current_year:
raise ValueError('卡片已過期')
if v > current_year + 20:
raise ValueError('年份不合理')
return v
@field_validator('cvv')
@classmethod
def validate_cvv(cls, v: str) -> str:
if not v.isdigit() or not 3 <= len(v) <= 4:
raise ValueError('CVV 必須是 3-4 位數字')
return v
@field_validator('holder_name')
@classmethod
def validate_holder_name(cls, v: str) -> str:
v = v.strip().upper()
if not re.match(r'^[A-Z\s]+$', v):
raise ValueError('持卡人姓名只能包含英文字母')
return v
@model_validator(mode='after')
def check_expiry(self):
today = date.today()
if self.expiry_year == today.year and self.expiry_month < today.month:
raise ValueError('卡片已過期')
return self訂單驗證
from pydantic import BaseModel, Field, field_validator, model_validator
from typing import List, Optional
from decimal import Decimal
from datetime import datetime
from enum import Enum
class OrderStatus(str, Enum):
PENDING = "pending"
CONFIRMED = "confirmed"
SHIPPED = "shipped"
DELIVERED = "delivered"
CANCELLED = "cancelled"
class OrderItem(BaseModel):
product_id: int = Field(gt=0)
product_name: str = Field(min_length=1)
quantity: int = Field(ge=1, le=999)
unit_price: Decimal = Field(ge=0, decimal_places=2)
@property
def subtotal(self) -> Decimal:
return self.unit_price * self.quantity
class ShippingAddress(BaseModel):
recipient: str = Field(min_length=1, max_length=100)
phone: str = Field(pattern=r'^09\d{8}$')
city: str
district: str
address: str = Field(min_length=5, max_length=200)
zip_code: str = Field(pattern=r'^\d{3,5}$')
class CreateOrderRequest(BaseModel):
items: List[OrderItem] = Field(min_length=1, max_length=50)
shipping_address: ShippingAddress
coupon_code: Optional[str] = Field(None, pattern=r'^[A-Z0-9]{6,12}$')
note: Optional[str] = Field(None, max_length=500)
@field_validator('items')
@classmethod
def validate_items(cls, v: List[OrderItem]) -> List[OrderItem]:
# 檢查是否有重複的商品
product_ids = [item.product_id for item in v]
if len(product_ids) != len(set(product_ids)):
raise ValueError('訂單中有重複的商品')
return v
@model_validator(mode='after')
def validate_order_total(self):
# 計算總金額
total = sum(item.subtotal for item in self.items)
# 檢查最低訂單金額
if total < Decimal('100'):
raise ValueError('訂單金額必須至少 100 元')
# 檢查最高訂單金額
if total > Decimal('1000000'):
raise ValueError('訂單金額超過上限')
return self
@property
def total_amount(self) -> Decimal:
return sum(item.subtotal for item in self.items)
@property
def total_items(self) -> int:
return sum(item.quantity for item in self.items)⚠️ 常見錯誤
1. 忘記 @classmethod
# ❌ 錯誤
class User(BaseModel):
name: str
@field_validator('name')
def validate_name(cls, v): # 缺少 @classmethod
return v
# ✅ 正確
class User(BaseModel):
name: str
@field_validator('name')
@classmethod
def validate_name(cls, v):
return v2. 驗證器順序
# 注意:欄位驗證器按照欄位定義順序執行
class User(BaseModel):
password: str
confirm: str # 這個驗證器執行時,password 已經驗證過了
@field_validator('confirm')
@classmethod
def check_confirm(cls, v, info):
if 'password' in info.data: # password 已可用
...
return v3. 修改 self 在 model_validator
# ❌ mode='before' 不能存取 self
@model_validator(mode='before')
@classmethod
def validate(cls, values): # values 是 dict
return values
# ✅ mode='after' 可以存取 self
@model_validator(mode='after')
def validate(self): # 是實例方法
self.field = self.field.upper()
return self✅ 重點總結
驗證器類型
| 驗證器 | 用途 | mode |
|---|---|---|
field_validator | 單一欄位 | before/after |
model_validator | 整個模型 | before/after/wrap |
Annotated | 可重用驗證 | BeforeValidator/AfterValidator |
執行順序
model_validator(mode='before')- 原始輸入field_validator(mode='before')- 型別轉換前- 核心型別驗證和轉換
field_validator(mode='after')- 型別轉換後model_validator(mode='after')- 所有欄位驗證後
🎤 面試這樣答
Q: Pydantic 中 field_validator 和 model_validator 有什麼區別?
答案:
field_validator用於驗證單一欄位,只能存取該欄位的值和已經驗證過的其他欄位。
model_validator用於驗證整個模型,可以存取所有欄位,適合做跨欄位驗證,例如確認密碼是否一致。@field_validator('email') @classmethod def validate_email(cls, v): return v.lower() @model_validator(mode='after') def check_passwords_match(self): if self.password != self.confirm_password: raise ValueError('密碼不一致') return self
🤓 小測驗
@field_validator需要搭配什麼裝飾器?如何在 field_validator 中存取其他欄位的值?
mode=‘before’ 和 mode=‘after’ 的差別?
上一篇: 02-2. 欄位驗證與約束 下一篇: 02-4. 巢狀模型與繼承
最後更新:2025-12-17