本文 AI 產出,尚未審核

FastAPI 教學:Pydantic 模型中的 Request Body 驗證


簡介

在 Web API 開發中,請求資料的正確性與安全性是最基礎也是最重要的需求之一。若沒有適當的驗證機制,錯誤或惡意資料就可能直接進入後端,導致程式錯誤、資料庫污染,甚至安全漏洞。FastAPI 以 Pydantic 為核心,提供了強大且直觀的資料模型與驗證功能,使得開發者只需要描述資料結構,就能自動完成 JSON 解析、型別檢查、欄位限制等工作。

本單元聚焦於 Request Body 的驗證。我們將從概念說明、實作範例、常見陷阱與最佳實踐,最後帶入實務應用情境,幫助你在 FastAPI 專案中快速建立可靠的 API。


核心概念

1. Pydantic 模型是什麼?

  • Pydantic 是一套基於 Python type hints 的資料驗證與設定管理庫。
  • 在 FastAPI 中,我們透過繼承 pydantic.BaseModel 來宣告 Request/Response 模型
  • Pydantic 會在收到請求時自動 解析 JSON轉型(例如把字串轉成 datetime)並 驗證欄位規則。若驗證失敗,FastAPI 會回傳 422 Unprocessable Entity 錯誤,且錯誤訊息非常清晰。

2. 基本使用方式

from pydantic import BaseModel

class Item(BaseModel):
    name: str          # 必填,字串
    price: float       # 必填,浮點數
    description: str | None = None   # 可選,預設為 None

在路由函式中直接使用模型作為參數:

from fastapi import FastAPI

app = FastAPI()

@app.post("/items/")
async def create_item(item: Item):
    # `item` 已經是驗證過的 Pydantic 物件
    return {"msg": "建立成功", "item": item}

3. 欄位驗證 – 內建限制

Pydantic 內建多種驗證器,常見的有:

限制類型 範例 說明
conintconfloat price: conint(gt=0) 限制整數/浮點數的大小
constr name: constr(min_length=3, max_length=50) 限制字串長度
EmailStr email: EmailStr 自動驗證 Email 格式
HttpUrl url: HttpUrl 驗證 URL
datetime created_at: datetime 解析 ISO8601 日期時間

4. 自訂驗證 – validator

有時候內建限制不足,我們可以使用 @validator 撰寫自訂規則。

from pydantic import validator

class User(BaseModel):
    username: str
    password: str

    @validator("password")
    def password_complexity(cls, v):
        if len(v) < 8:
            raise ValueError("密碼長度至少 8 個字元")
        if not any(c.isdigit() for c in v):
            raise ValueError("密碼必須包含數字")
        return v

5. 嵌套模型與 List

Pydantic 支援 模型嵌套陣列,讓複雜資料結構的驗證變得簡單。

from typing import List

class OrderItem(BaseModel):
    product_id: int
    quantity: conint(gt=0)

class Order(BaseModel):
    customer_id: int
    items: List[OrderItem]    # 多筆商品

程式碼範例

以下提供 4 個實務中常見的範例,從最簡單到稍微進階,說明如何在 FastAPI 中使用 Pydantic 進行 Request Body 驗證。

範例 1:最基礎的資料驗證

# file: main.py
from fastapi import FastAPI
from pydantic import BaseModel, conint, constr

app = FastAPI()

class Product(BaseModel):
    name: constr(min_length=2, max_length=100)
    price: conint(gt=0)          # 價格必須是正整數
    stock: conint(ge=0) = 0      # 庫存可為 0,但不可為負

@app.post("/products/")
async def create_product(product: Product):
    """
    FastAPI 會自動檢查:
    - name 長度是否符合
    - price 是否大於 0
    - stock 是否大於等於 0
    若驗證失敗,回傳 422 並列出錯誤細節。
    """
    return {"msg": "商品已建立", "product": product}

重點說明

  • constrconint內建限制,寫法簡潔。
  • 欄位預設值 (stock) 直接在模型中設定,讓 API 呼叫方可選擇是否提供。

範例 2:使用 EmailStr 與自訂驗證

# file: users.py
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, EmailStr, validator

app = FastAPI()

class RegisterUser(BaseModel):
    email: EmailStr
    password: str
    confirm_password: str

    @validator("confirm_password")
    def passwords_match(cls, v, values, **kwargs):
        if "password" in values and v != values["password"]:
            raise ValueError("密碼與確認密碼不一致")
        return v

@app.post("/register/")
async def register(user: RegisterUser):
    # 假設此處會寫入資料庫
    return {"msg": "註冊成功", "email": user.email}

重點說明

  • EmailStr 會自動檢查 Email 格式。
  • @validator 允許 跨欄位驗證(此例驗證兩個密碼欄位相同)。
  • 若驗證失敗,FastAPI 仍回傳 422,錯誤訊息會指出是哪個欄位出問題。

範例 3:嵌套模型 + List

# file: orders.py
from fastapi import FastAPI
from pydantic import BaseModel, conint
from typing import List

app = FastAPI()

class OrderItem(BaseModel):
    product_id: int
    quantity: conint(gt=0)   # 數量必須大於 0

class OrderCreate(BaseModel):
    customer_id: int
    items: List[OrderItem]   # 至少要有一筆商品

    @validator("items")
    def at_least_one_item(cls, v):
        if not v:
            raise ValueError("訂單必須至少包含一項商品")
        return v

@app.post("/orders/")
async def create_order(order: OrderCreate):
    # 這裡可以直接使用 order.items 進行庫存扣減等商業邏輯
    return {"msg": "訂單已建立", "order_id": 12345}

重點說明

  • List[OrderItem] 讓 FastAPI 能夠 遞迴驗證 每筆子項目。
  • 透過 @validator 檢查 集合的整體條件(至少一項商品)。
  • 若子項目有錯誤,錯誤訊息會顯示在 items.{index}.field 的路徑上,方便前端定位。

範例 4:自訂 JSON Schema、回傳錯誤訊息

# file: products.py
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
from pydantic import BaseModel, Field, ValidationError

app = FastAPI()

class NewProduct(BaseModel):
    name: str = Field(..., min_length=2, max_length=80, description="商品名稱")
    price: float = Field(..., gt=0, description="商品價格,必須大於 0")
    tags: list[str] | None = Field(default=None, description="可選的標籤列表")

@app.exception_handler(ValidationError)
async def validation_exception_handler(request: Request, exc: ValidationError):
    """
    自訂驗證失敗的回應結構,讓前端收到的錯誤更友好。
    """
    errors = [{"loc": e["loc"], "msg": e["msg"], "type": e["type"]} for e in exc.errors()]
    return JSONResponse(
        status_code=422,
        content={"detail": errors, "message": "請檢查傳入的資料格式"}
    )

@app.post("/products/advanced/")
async def create_advanced(product: NewProduct):
    # 此處已完成完整驗證
    return {"msg": "商品建立成功", "product": product}

重點說明

  • 使用 Field 可以在 JSON Schema 中加入說明 (description) 與限制,讓自動產生的 OpenAPI 文件更完整。
  • 透過 @app.exception_handler(ValidationError) 自訂驗證失敗的回應格式,提升 API 的 可用性
  • 前端開發者只要依照 detail 陣列的 loc(位置)與 msg(訊息)即可快速呈現錯誤。

常見陷阱與最佳實踐

陷阱 說明 解決方案 / 最佳實踐
忘記在路由函式中加入模型參數 若只寫 def foo(): 而不把模型寫在參數裡,FastAPI 不會自動驗證。 必須Model 作為函式參數,或使用 Body(...) 明確指示。
使用可變預設值 (mutable default) tags: list[str] = [] 會在所有請求間共享同一個列表。 使用 `list[str]
忽視跨欄位驗證 單一欄位驗證不能保證資料邏輯正確(例如密碼與確認密碼不一致)。 利用 @validator 並設 always=True 進行 跨欄位 檢查。
過度依賴 Any 把欄位型別寫成 Any 會失去驗證的意義。 盡量使用具體型別或自訂驗證,保留 Any 僅在真的無法預測時使用。
未處理嵌套模型的錯誤訊息 複雜結構錯誤時前端可能只看到「validation error」而無法定位。 自訂例外處理(如上例)或在前端使用 error.loc 解析路徑。
忘記設定 response_model 輸出資料未經模型過濾,可能洩漏內部欄位。 為每個路由 明確指定 response_model,並使用 exclude_unset=True

其他最佳實踐

  1. 盡量在模型層完成驗證:讓路由函式只負責業務邏輯,避免在端點內手動檢查。
  2. 使用 Configjson_encoders:若要自訂特殊型別(如 Decimal)的序列化方式。
  3. 分層模型:例如 UserCreateUserUpdateUserRead,避免同一模型同時承擔寫入與回傳需求。
  4. 加入 OpenAPI 註解:利用 Field(..., description="...")example=,提升 API 文件可讀性。
  5. 測試驗證邏輯:使用 TestClient 撰寫單元測試,確保模型驗證在未來變更時仍正確。

實際應用場景

1. 電子商務平台的商品上架

  • 需求:商品名稱、價格、庫存、分類、標籤等必須在上架時驗證。
  • 實作:使用 constrconintconfloat 限制長度與數值範圍;tags 使用 list[str] 並加入 max_items 檢查。
  • 好處:前端錯誤即時回饋,後端免除大量手動驗證程式碼。

2. 會員系統的註冊與密碼重設

  • 需求:Email 必須符合 RFC 標準,密碼需符合強度規則,且兩次輸入必須相同。
  • 實作EmailStr + @validator 檢查密碼長度、數字、特殊字元,並比較 passwordconfirm_password
  • 好處:安全性提升,且錯誤訊息清楚,降低使用者流失。

3. 物流系統的批次匯入

  • 需求:一次上傳多筆訂單,每筆訂單包含多項商品,且每筆資料必須完整且符合商業規則。
  • 實作:使用 嵌套模型 + List,在 OrderCreate 中加入 @validator 確認至少一筆商品、庫存足夠等。
  • 好處:一次驗證整批資料,若有錯誤直接回傳第幾筆哪個欄位失敗,前端可快速定位。

4. 金融系統的交易請求

  • 需求:金額必須是正數且符合小數位限制(如最多兩位),交易日期必須是未來日期,且必須提供有效的銀行帳號。
  • 實作:使用 condecimal(max_digits=12, decimal_places=2, gt=0)、自訂 @validator 檢查日期、regex 驗證帳號格式。
  • 好處:在進入核心交易流程前即剔除不合法請求,降低錯誤風險與資安問題。

總結

  • Pydantic 為 FastAPI 提供了 聲明式、類型安全 的資料驗證機制,讓 API 開發者可以把注意力集中在業務邏輯上。
  • 透過 內建限制conintconstrEmailStr 等)與 自訂驗證@validator),我們可以輕鬆完成從簡單欄位檢查到跨欄位、嵌套結構的完整驗證。
  • 最佳實踐 包括:使用 Field 加入說明與範例、避免可變預設值、為每個端點明確指定 response_model、自訂例外處理以提升錯誤回饋品質。
  • 在實務上,從 電商商品上架會員註冊批次匯入金融交易,正確的 Request Body 驗證都是保證系統穩定與安全的第一道防線。

掌握上述概念與技巧,你就能在 FastAPI 中快速構建 可靠、易維護 的 API,為後續的功能擴充與團隊協作奠定堅實基礎。祝開發順利,期待看到你打造出更安全、更友善的服務! 🚀