FastAPI 課程 – Pydantic 模型(Request / Response Models)
主題:驗證錯誤自動產生(ValidationError)
簡介
在 FastAPI 中,所有的輸入與輸出都會透過 Pydantic 模型來描述與驗證。當前端送來的 JSON、表單資料或路徑參數不符合模型定義時,Pydantic 會自動拋出 ValidationError,而 FastAPI 會把這個錯誤轉換成符合 OpenAPI 規範的 HTTP 422 Unprocessable Entity 回應。
自動產生的驗證錯誤不僅讓 API 開發者免去手寫大量檢查程式碼,也為前端提供 結構化、易於解析 的錯誤訊息,提升整體開發效率與使用者體驗。因此,了解 ValidationError 的運作原理、常見陷阱與最佳實踐,是使用 FastAPI 的必備功力。
核心概念
1. Pydantic 模型的基本結構
Pydantic 以 type hints(型別註解)作為驗證依據,支援 Python 原生型別、typing 模組以及自訂驗證器。範例:
from pydantic import BaseModel, Field, EmailStr
class UserCreate(BaseModel):
username: str = Field(..., min_length=3, max_length=20, description="使用者名稱")
password: str = Field(..., min_length=6, description="密碼,至少 6 個字元")
email: EmailStr = Field(..., description="有效的電子郵件")
...表示此欄位為必填。Field可加入限制條件(長度、正則、描述等),這些條件會在驗證階段自動套用。
2. 當驗證失敗時的 ValidationError
如果請求的 JSON 不符合 UserCreate 的規範,Pydantic 會拋出 ValidationError,FastAPI 會捕捉並回傳如下的 JSON:
{
"detail": [
{
"loc": ["body", "username"],
"msg": "ensure this value has at least 3 characters",
"type": "value_error.any_str.min_length",
"ctx": {"limit_value": 3}
},
{
"loc": ["body", "email"],
"msg": "value is not a valid email address",
"type": "value_error.email"
}
]
}
loc:錯誤發生的位置(body、query、path…)。msg:人類可讀的錯誤訊息。type:機器可辨識的錯誤類型。
3. 自訂錯誤訊息與驗證器
有時候內建的錯誤訊息不符合業務需求,我們可以透過 @validator 或 field_error_msg 來自訂:
from pydantic import validator
class Product(BaseModel):
name: str
price: float
@validator('price')
def price_must_be_positive(cls, v):
if v <= 0:
raise ValueError('價格必須大於 0')
return v
此時若傳入 price: -5,回傳的錯誤訊息會是 「價格必須大於 0」。
4. 多層結構與嵌套模型
在實務上,API 常常需要回傳或接收多層次的資料。只要把模型嵌套即可,驗證錯誤仍會保持完整的路徑資訊:
class Address(BaseModel):
city: str
zip_code: str = Field(..., regex=r'^\d{5}$')
class UserProfile(BaseModel):
user: UserCreate
address: Address
若 zip_code 不符合 5 位數的正則,錯誤 loc 會是 ["body", "address", "zip_code"],讓前端可以精確定位問題。
5. 回應模型的自動驗證
FastAPI 也會對 response model 進行驗證,確保程式碼實際回傳的資料符合宣告的結構。這對於防止意外洩漏敏感欄位非常重要:
from fastapi import FastAPI
from fastapi.responses import JSONResponse
app = FastAPI()
class UserOut(BaseModel):
id: int
username: str
email: EmailStr
@app.get("/users/{user_id}", response_model=UserOut)
async def get_user(user_id: int):
# 假設從資料庫取得的 dict 少了 email 欄位
raw_user = {"id": user_id, "username": "alice"}
return JSONResponse(content=raw_user) # ValidationError 會在此拋出
若回傳資料缺少 email,FastAPI 會在開發環境直接拋出 ValidationError,提醒開發者修正。
程式碼範例
以下提供 5 個實作範例,逐步展示驗證錯誤的自動產生與客製化技巧。
範例 1:最簡單的 Request 驗證
# main.py
from fastapi import FastAPI
from pydantic import BaseModel, Field
app = FastAPI()
class Item(BaseModel):
name: str = Field(..., min_length=1)
quantity: int = Field(..., gt=0)
@app.post("/items")
async def create_item(item: Item):
return {"msg": "建立成功", "item": item}
- 測試:使用
curl -X POST -H "Content-Type: application/json" -d '{"name":"","quantity":-1}' http://localhost:8000/items - 結果:會得到 422 回應,
detail包含兩筆錯誤(name長度不足、quantity必須大於 0)。
範例 2:自訂錯誤訊息
from pydantic import BaseModel, validator
class Register(BaseModel):
username: str
password: str
@validator('password')
def strong_password(cls, v):
if len(v) < 8:
raise ValueError('密碼長度至少 8 個字元')
if not any(c.isdigit() for c in v):
raise ValueError('密碼必須包含數字')
return v
- 當前端傳入
password: "abc"時,回傳的msg會是 「密碼長度至少 8 個字元」,而非預設的「ensure this value has at least 8 characters」。
範例 3:嵌套模型與錯誤定位
class OrderItem(BaseModel):
product_id: int
amount: int = Field(..., gt=0)
class Order(BaseModel):
user_id: int
items: list[OrderItem]
@app.post("/orders")
async def create_order(order: Order):
return {"status": "ok", "order_id": 123}
- 測試傳入
items中的某筆amount為0,回傳的loc為["body", "items", 0, "amount"],清楚指出出錯的陣列索引與欄位。
範例 4:Response Model 的驗證
class TokenOut(BaseModel):
access_token: str
token_type: str = "bearer"
@app.post("/login", response_model=TokenOut)
async def login(username: str, password: str):
# 假設產生的 token 為 None(程式錯誤)
token = None
return {"access_token": token}
- 在開發模式下,FastAPI 會拋出
ValidationError,提示access_token欄位值不符合str型別,協助開發者快速發現錯誤。
範例 5:全局錯誤處理與自訂回應
有時候我們想把 ValidationError 包裝成前端慣用的錯誤格式(例如 code, message, fields):
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse
from fastapi import Request
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
errors = []
for err in exc.errors():
errors.append({
"field": ".".join(str(loc) for loc in err["loc"][1:]), # 移除 "body" 前綴
"error": err["msg"]
})
return JSONResponse(
status_code=422,
content={"code": "VALIDATION_ERROR", "message": "請檢查輸入欄位", "errors": errors},
)
- 此處理器會把原始的
detail重新組裝成code、message、errors三層結構,前端只要讀取errors陣列即可呈現每筆欄位錯誤。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方案 |
|---|---|---|
忘記加 ...(必填) |
欄位預設為 None,導致不會觸發必填驗證。 |
使用 Field(... ) 或在型別後加 ...。 |
| 正則表達式寫錯 | 錯誤的 regex 會讓所有輸入都失敗,難以除錯。 | 使用 re.compile 測試,或先在線上 regex 測試工具驗證。 |
| Response Model 與實際回傳不一致 | 在開發階段未觸發驗證,導致生產環境洩漏資訊。 | 開啟 debug=True 或在測試環境使用 TestClient 執行所有路由,確保驗證生效。 |
| 嵌套模型錯誤訊息過長 | 前端只需要欄位名稱,卻得到完整路徑。 | 使用全局例外處理器,將 loc 轉換成更友善的欄位路徑(如 address.zip_code)。 |
使用 Any 或 Dict 失去驗證 |
這類型別會跳過 Pydantic 的檢查。 | 盡量使用具體模型或 TypedDict,必要時自行在 @validator 中加入檢查。 |
最佳實踐:
- 盡量在模型層完成驗證,避免在路由函式內手寫檢查。
- 為每個欄位加上說明 (
description),OpenAPI 文件會自動呈現,提升 API 可讀性。 - 使用
@root_validator處理跨欄位的商業規則(例如「開始日期必須早於結束日期」)。 - 在測試階段使用
TestClient進行端到端驗證,確保ValidationError行為如預期。 - 統一錯誤回應格式,透過全局例外處理器或自訂
HTTPException,讓前端開發者只需要寫一次錯誤處理邏輯。
實際應用場景
1. 電子商務平台的訂單建立
- 需求:使用者必須提供有效的收貨地址、商品清單、付款資訊。
- 做法:定義
OrderCreate、Address、PaymentInfo等多層模型,讓 Pydantic 自動驗證每個欄位。若地址的郵遞區號不符格式,前端會立即收到zip_code錯誤,無需額外的後端檢查。
2. 會員系統的密碼強度檢查
- 需求:密碼必須包含大小寫、數字與特殊字元,且長度至少 12。
- 做法:在模型中加入
@validator('password'),拋出自訂ValueError。所有密碼相關的 API(註冊、重設)共用同一模型,確保規則一致。
3. 多語系 API 回傳結構的保護
- 需求:根據使用者語系回傳不同的
message欄位,但必須保證每個語系都有對應文字。 - 做法:使用
Dict[str, str]搭配@validator檢查鍵值集合是否包含所有支援語系,若缺少則拋出ValidationError,避免因遺漏文字導致前端 UI 顯示空白。
總結
- Pydantic 為 FastAPI 提供了 宣告式驗證,讓開發者只需描述資料結構,即可自動生成
ValidationError。 - 透過
Field、@validator、嵌套模型,我們可以在不同層級、不同情境下完成精細的資料檢查。 - FastAPI 會把驗證錯誤轉換成符合 OpenAPI 的 422 回應,而且對 response model 也會進行驗證,保護 API 不會意外回傳錯誤或敏感資料。
- 為了提升開發效率與使用者體驗,建議:
- 在模型層完成所有驗證,避免在路由裡重複撰寫檢查程式。
- 自訂錯誤訊息 讓前端更易於呈現。
- 使用全局例外處理器 統一錯誤回傳格式。
- 在測試階段使用 TestClient 確認驗證行為。
掌握了 ValidationError 的自動產生與客製化技巧後,你的 FastAPI 專案將會變得更安全、更易於維護,也能為前端團隊提供即時、結構化的錯誤回饋,提升整體開發效率。祝你在 FastAPI 的旅程中玩得開心、寫得順利! 🚀