本文 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 為欄位的原始值。
  • 若拋出 ValueErrorTypeErrorAssertionError,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=Truealways=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 只接受 ValueErrorTypeErrorAssertionError,其他例外會變成 500 錯誤。 使用 raise ValueError(...) 進行驗證失敗。
重複驗證 多個 validator 針對相同欄位執行相同檢查,增加維護成本。 合併驗證邏輯或抽成共用函式。
忘記 always=True 欄位缺失時 validator 不會執行,導致預設值未被正確處理。 若需要在缺失時也跑驗證,加入 always=True

最佳實踐

  1. 保持 validator 簡潔:每個 validator 只做一件事,讓錯誤訊息更明確。
  2. 使用自訂例外類別(可選):若想在 API 回傳更結構化的錯誤,可自訂 PydanticErrorMixin
  3. 將驗證邏輯抽成工具函式:讓模型保持乾淨,且方便單元測試。
  4. 搭配型別提示:利用 typing(如 Literalconint)減少手寫 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
  • 場景說明:前端可能分別送出 datetime,透過 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_validatorpre=Truealways=True 等參數,你可以靈活地處理 單欄位跨欄位前置清理後置轉換
  • 避免常見陷阱(忘記回傳值、過度使用 pre、不當的例外類型),遵守 單一職責抽離共用函式 的最佳實踐,能讓模型既可讀可維護
  • 在電商、金融、會員等實務場景中,適當運用 validators 能大幅降低業務層的防呆邏輯,提升 API 的一致性與安全性。

掌握了這些技巧後,你就能在 FastAPI 專案中寫出乾淨、可靠且易於擴充的資料驗證程式,為整體系統品質奠定堅實基礎。祝開發順利 🚀