本文 AI 產出,尚未審核

FastAPI

單元:資料驗證與轉換(Validation & Serialization)

主題:自訂驗證邏輯


簡介

FastAPI 中,資料驗證與序列化幾乎全靠 Pydantic 模型完成。內建的型別檢查、範圍限制、正則表達式等已能滿足大多數需求,但實務開發常會碰到 「這個欄位必須符合特定商業規則」「兩個欄位之間需要相互驗證」 等更複雜的情況。

若僅依賴 Pydantic 提供的預設驗證,往往會讓 API 的錯誤訊息不夠精確,或是驗證邏輯散落在多個路由函式中,難以維護。透過 自訂驗證邏輯(custom validators),我們可以:

  1. 把業務規則集中在模型內,保持路由函式的簡潔。
  2. 讓錯誤回傳結構化且具可讀性,提升前端開發者的除錯效率。
  3. 充分利用型別提示(type hints),讓 IDE 能即時提示錯誤。

本篇文章將一步步說明如何在 FastAPI 中撰寫、測試與最佳化自訂驗證,並提供多個實用範例,幫助你快速上手。


核心概念

1. Pydantic 的 @validator 裝飾器

@validator 是 Pydantic 提供的主要切入點,允許在欄位賦值前執行任意 Python 程式碼。其基本語法如下:

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('年齡必須是正數')
        return v
  • cls:模型類別本身,通常不需要使用。
  • v:欄位的原始值。
  • 回傳值:必須是驗證後的值,否則會拋出例外。

重點:若同時驗證多個欄位,可使用 @validator('*')pre=True 參數,讓驗證在欄位解析前(pre)或後(post)執行。

2. 欄位間的相依驗證

有時驗證需要參考其他欄位,例如「密碼與確認密碼必須相同」或「開始日期必須早於結束日期」。這時可以使用 @root_validator,它會收到整個模型的資料字典。

from pydantic import BaseModel, root_validator

class PasswordReset(BaseModel):
    password: str
    confirm_password: str

    @root_validator
    def passwords_match(cls, values):
        pw, cpw = values.get('password'), values.get('confirm_password')
        if pw != cpw:
            raise ValueError('密碼與確認密碼不一致')
        return values

3. 使用 pre=True 進行前置驗證

pre=True 讓驗證在 Pydantic 解析欄位之前執行,適合處理 字串、JSON 文字或自訂格式 的前置清理。

class Item(BaseModel):
    tags: list[str]

    @validator('tags', pre=True)
    def split_tags(cls, v):
        # 前端可能傳入 "tag1,tag2, tag3"
        if isinstance(v, str):
            return [t.strip() for t in v.split(',')]
        return v

4. 自訂資料型別(Custom Types)

若驗證邏輯非常複雜,或需要在多個模型間重複使用,可以定義 自訂資料型別,並實作 __get_validators__ 方法。

class PhoneNumber(str):
    @classmethod
    def __get_validators__(cls):
        yield cls.validate

    @classmethod
    def validate(cls, v):
        import re
        if not re.fullmatch(r'\+?\d{10,15}', v):
            raise ValueError('電話號碼格式不正確')
        return cls(v)

在模型中直接使用:

class Contact(BaseModel):
    name: str
    phone: PhoneNumber

5. 例外類型與錯誤訊息客製化

Pydantic 允許拋出 ValueErrorTypeError,或自訂的例外類別。若想要 回傳結構化的錯誤訊息(例如包含錯誤碼),可繼承 pydantic.errors.PydanticErrorMixin

from pydantic.errors import PydanticErrorMixin

class InvalidCurrencyError(PydanticErrorMixin, ValueError):
    code = 'currency.invalid'
    msg_template = '不支援的貨幣代碼:{currency}'

在驗證器中拋出:

if currency not in ('USD', 'TWD', 'EUR'):
    raise InvalidCurrencyError(currency=currency)

FastAPI 會自動把 code 轉成 JSON 回傳,前端只要根據 code 做對應處理即可。


程式碼範例

以下提供 五個 常見且實用的自訂驗證範例,涵蓋欄位驗證、相依驗證、前置清理、共用型別與錯誤客製化。

範例 1:Email 與 Domain 限制

from pydantic import BaseModel, EmailStr, validator

class RegisterUser(BaseModel):
    email: EmailStr
    allowed_domains: list[str] = ['example.com', 'mycompany.org']

    @validator('email')
    def email_must_be_allowed_domain(cls, v, values):
        domain = v.split('@')[-1]
        if domain not in values.get('allowed_domains', []):
            raise ValueError(f'不允許使用 {domain} 的郵件域名')
        return v

說明:先用 EmailStr 確保格式正確,再檢查是否屬於允許的 domain。values 參數讓我們存取同一模型的其他欄位(此例為 allowed_domains)。


範例 2:日期範圍驗證(開始 < 結束)

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_time_order(cls, values):
        if values['start_at'] >= values['end_at']:
            raise ValueError('活動開始時間必須早於結束時間')
        return values

說明:使用 root_validator 同時檢查兩個欄位的相對關係,避免在路由內寫冗長條件式。


範例 3:前置清理多重標籤(CSV → List)

from pydantic import BaseModel, validator

class BlogPost(BaseModel):
    title: str
    tags: list[str]

    @validator('tags', pre=True)
    def parse_tags(cls, v):
        # 前端可能傳入 "python, fastapi ,web"
        if isinstance(v, str):
            return [tag.strip() for tag in v.split(',') if tag.strip()]
        return v

說明pre=True 讓我們在 Pydantic 解析 list[str] 前先把字串切割、去空白,提升 API 的彈性。


範例 4:共用自訂型別 – 台灣身分證字號

import re
from pydantic import BaseModel, errors

class TaiwanID(str):
    @classmethod
    def __get_validators__(cls):
        yield cls.validate

    @classmethod
    def validate(cls, v):
        if not isinstance(v, str):
            raise TypeError('身分證必須是字串')
        pattern = r'^[A-Z][12]\d{8}$'
        if not re.fullmatch(pattern, v):
            raise ValueError('身分證格式不正確')
        return cls(v)

class Citizen(BaseModel):
    name: str
    nid: TaiwanID

說明:將驗證邏輯封裝成自訂型別後,任何模型只要使用 TaiwanID 就自動得到相同的驗證與錯誤訊息。


範例 5:客製化錯誤碼 – 貨幣代碼驗證

from pydantic import BaseModel, validator
from pydantic.errors import PydanticErrorMixin

class CurrencyError(PydanticErrorMixin, ValueError):
    code = 'currency.invalid'
    msg_template = '不支援的貨幣代碼:{currency}'

class Payment(BaseModel):
    amount: float
    currency: str

    @validator('currency')
    def validate_currency(cls, v):
        allowed = {'USD', 'TWD', 'JPY'}
        if v not in allowed:
            raise CurrencyError(currency=v)
        return v

說明:拋出 CurrencyError 後,FastAPI 回傳的 JSON 會包含 code 欄位,前端可依此顯示對應的多語系錯誤訊息。


常見陷阱與最佳實踐

陷阱 可能的後果 解決方案 / 最佳實踐
@validator 中直接修改其他欄位 造成驗證順序不確定,可能導致資料不一致 使用 @root_validatorpre=True 只做「讀取」與「返回」的工作,避免副作用
忘記回傳驗證後的值 Pydantic 會回傳 None,導致欄位變成 null 每個 validator 必須 return v(或處理後的值)
在模型外部重複寫相同驗證 程式碼難以維護,錯誤訊息不一致 把驗證抽成 自訂型別共用函式,在多個模型間重用
使用過於寬鬆的正則表達式 讓不合法資料通過,安全風險上升 先寫單元測試驗證正則表達式的邊界情況,必要時加上額外的 Python 檢查
拋出非 Pydantic 例外 FastAPI 會把例外轉成 500 錯誤,失去結構化錯誤訊息 盡量拋出 ValueErrorTypeError 或自訂的 PydanticErrorMixin 例外

最佳實踐總結

  1. 驗證層級:欄位驗證 → 相依驗證 → 前置清理。依序使用 @validator(pre=True)@validator@root_validator
  2. 錯誤訊息:保持訊息簡潔、具體,必要時加入 code 讓前端快速對應。
  3. 測試:使用 pytest 為每個 validator 撰寫單元測試,確保邊界條件不會被遺漏。
  4. 文件:在模型的 docstring 中說明每個自訂驗證的商業規則,方便團隊成員快速了解。

實際應用場景

1. 電子商務 – 商品上架規則

  • 必須保證 price 大於 0 且小於 max_price(由系統設定)。
  • sku 必須符合特定格式(英數混合、長度 8)。
  • available_from 必須早於 available_to,且兩者皆不能早於今天。

透過 @validator@root_validator,所有規則可集中在 Product 模型內,API 路由只負責接收與回傳。

2. 金融系統 – 交易驗證

  • 交易金額必須在每日上限內。
  • currency 必須是白名單內的代碼,且不同幣別的最小交易單位不同。
  • 交易時間必須在營業時間(09:00–17:00)內。

使用 自訂型別 (CurrencyAmount) 結合 客製化錯誤碼,讓前端 UI 能即時顯示「金額過高」或「不支援的幣別」等訊息。

3. 會員系統 – 密碼政策

  • 密碼必須同時包含大小寫、數字與特殊字元,且長度至少 12。
  • 新密碼不能與最近三次使用過的密碼相同(需要查資料庫)。

@validator('password') 只負責格式檢查,@root_validator 再呼叫服務層的資料庫檢查,保持模型的純粹性。


總結

  • 自訂驗證 是 FastAPI 與 Pydantic 強大功能的核心,讓開發者可以把複雜的商業規則寫在模型層,保持路由乾淨、可測試。
  • 透過 @validator@root_validatorpre=True、自訂型別與錯誤碼,我們能夠 精準控制資料的有效性、提供結構化錯誤回應,並在多個模型間 重用驗證邏輯
  • 避免常見陷阱(副作用、忘記回傳值、錯誤訊息不一致),遵守最佳實踐(層級分明、單元測試、文件化),即可建構 安全、易維護且使用者友好的 API

掌握這些技巧後,你的 FastAPI 專案將能在 資料驗證 這一關鍵環節上更上一層樓,為後續功能擴充奠定堅實基礎。祝開發順利!