本文 AI 產出,尚未審核

FastAPI 課程 – Pydantic v2 的 model_validator()field_validator()


簡介

在 FastAPI 中,Pydantic 是負責資料驗證與序列化的核心工具。從 Pydantic v2 開始,舊有的 @validator@root_validator 已被全新機制取代:field_validator()model_validator()。這兩個 decorator 不僅語法更直觀,也支援 同步 / 非同步 的驗證流程,讓開發者能更輕鬆地寫出可讀性高、效能佳的驗證程式碼。

本單元將說明:

  1. 什麼是 field_validator()model_validator(),它們的差異與使用時機。
  2. Request / Response Model 中如何運用這兩個 validator,確保 API 收到的資料符合商業規則。
  3. 常見的陷阱、最佳實踐以及實務應用情境。

核心概念

1. 為什麼需要新驗證機制?

  • 更清晰的作用範圍field_validator() 只負責單一欄位的前後處理,model_validator() 則負責整個模型層級的驗證(類似舊版的 root_validator)。
  • 支援非同步:在需要呼叫外部 API、資料庫或其他 I/O 操作的驗證時,只要把 validator 定義成 async def 即可。
  • 型別安全:Pydantic v2 內建 TypedDictAnnotated 支援,讓驗證過程更貼合型別提示。

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_validatorfield_validator(..., mode='after') 中取得 values 使用 model_validator 或把驗證邏輯搬到 after 模式。
非同步 validator 被同步呼叫 若在同步函式中使用 async validator,會拋出 RuntimeError 確保 FastAPI 路由是 async,或改寫為同步。
過度驗證 把所有商業規則都塞進模型,導致模型過於龐大、難以維護。 領域規則放在服務層(service layer),僅保留資料完整性於模型。
未考慮 None field_validator 預設會在 None 時跳過驗證。 若需要檢查 None,加上 check_fields=False 或自行處理。

最佳實踐

  1. 分層驗證:資料型別驗證放在 Pydantic,商業邏輯放在服務層。
  2. 使用 mode='before' 進行預處理,例如字串去除空白、轉換時間格式。
  3. 盡量保持 validator 簡潔:每個 validator 只做一件事,方便單元測試。
  4. 加入型別提示def validator(cls, v: str) -> str,讓 IDE 能提供即時錯誤提示。
  5. 測試非同步 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 v2field_validator()model_validator() 取代舊版的 @validator@root_validator,提供 更清晰的驗證層級非同步支援
  • 在 FastAPI 中,將這兩個 validator 融入 Request / Response Model,可以在 API 入口即完成資料完整性與基本商業規則的檢查,減少後端服務層的防禦性程式碼。
  • 了解 mode='before' / mode='after' 的差異、正確使用 values、以及何時採用 同步 vs 非同步,是寫好驗證程式的關鍵。
  • 避免過度驗證、保持 validator 單一職責、並配合服務層的業務邏輯,能讓程式碼更易維護、測試更完整。

掌握了這套新驗證機制,你的 FastAPI 專案將能在 資料安全效能可讀性 上同時提升,為日後的功能擴充與團隊協作奠定堅實基礎。祝開發順利! 🚀