本文 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 內建多種驗證器,常見的有:
| 限制類型 | 範例 | 說明 |
|---|---|---|
conint、confloat |
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}
重點說明
constr、conint為 內建限制,寫法簡潔。- 欄位預設值 (
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。 |
其他最佳實踐
- 盡量在模型層完成驗證:讓路由函式只負責業務邏輯,避免在端點內手動檢查。
- 使用
Config的json_encoders:若要自訂特殊型別(如Decimal)的序列化方式。 - 分層模型:例如
UserCreate、UserUpdate、UserRead,避免同一模型同時承擔寫入與回傳需求。 - 加入 OpenAPI 註解:利用
Field(..., description="...")、example=,提升 API 文件可讀性。 - 測試驗證邏輯:使用
TestClient撰寫單元測試,確保模型驗證在未來變更時仍正確。
實際應用場景
1. 電子商務平台的商品上架
- 需求:商品名稱、價格、庫存、分類、標籤等必須在上架時驗證。
- 實作:使用
constr、conint、confloat限制長度與數值範圍;tags使用list[str]並加入max_items檢查。 - 好處:前端錯誤即時回饋,後端免除大量手動驗證程式碼。
2. 會員系統的註冊與密碼重設
- 需求:Email 必須符合 RFC 標準,密碼需符合強度規則,且兩次輸入必須相同。
- 實作:
EmailStr+@validator檢查密碼長度、數字、特殊字元,並比較password與confirm_password。 - 好處:安全性提升,且錯誤訊息清楚,降低使用者流失。
3. 物流系統的批次匯入
- 需求:一次上傳多筆訂單,每筆訂單包含多項商品,且每筆資料必須完整且符合商業規則。
- 實作:使用 嵌套模型 + List,在
OrderCreate中加入@validator確認至少一筆商品、庫存足夠等。 - 好處:一次驗證整批資料,若有錯誤直接回傳第幾筆哪個欄位失敗,前端可快速定位。
4. 金融系統的交易請求
- 需求:金額必須是正數且符合小數位限制(如最多兩位),交易日期必須是未來日期,且必須提供有效的銀行帳號。
- 實作:使用
condecimal(max_digits=12, decimal_places=2, gt=0)、自訂@validator檢查日期、regex驗證帳號格式。 - 好處:在進入核心交易流程前即剔除不合法請求,降低錯誤風險與資安問題。
總結
- Pydantic 為 FastAPI 提供了 聲明式、類型安全 的資料驗證機制,讓 API 開發者可以把注意力集中在業務邏輯上。
- 透過 內建限制(
conint、constr、EmailStr等)與 自訂驗證(@validator),我們可以輕鬆完成從簡單欄位檢查到跨欄位、嵌套結構的完整驗證。 - 最佳實踐 包括:使用
Field加入說明與範例、避免可變預設值、為每個端點明確指定response_model、自訂例外處理以提升錯誤回饋品質。 - 在實務上,從 電商商品上架、會員註冊、批次匯入 到 金融交易,正確的 Request Body 驗證都是保證系統穩定與安全的第一道防線。
掌握上述概念與技巧,你就能在 FastAPI 中快速構建 可靠、易維護 的 API,為後續的功能擴充與團隊協作奠定堅實基礎。祝開發順利,期待看到你打造出更安全、更友善的服務! 🚀