本文 AI 產出,尚未審核

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. 自訂錯誤訊息與驗證器

有時候內建的錯誤訊息不符合業務需求,我們可以透過 @validatorfield_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 中的某筆 amount0,回傳的 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 重新組裝成 codemessageerrors 三層結構,前端只要讀取 errors 陣列即可呈現每筆欄位錯誤。

常見陷阱與最佳實踐

陷阱 說明 解決方案
忘記加 ...(必填) 欄位預設為 None,導致不會觸發必填驗證。 使用 Field(... ) 或在型別後加 ...
正則表達式寫錯 錯誤的 regex 會讓所有輸入都失敗,難以除錯。 使用 re.compile 測試,或先在線上 regex 測試工具驗證。
Response Model 與實際回傳不一致 在開發階段未觸發驗證,導致生產環境洩漏資訊。 開啟 debug=True 或在測試環境使用 TestClient 執行所有路由,確保驗證生效。
嵌套模型錯誤訊息過長 前端只需要欄位名稱,卻得到完整路徑。 使用全局例外處理器,將 loc 轉換成更友善的欄位路徑(如 address.zip_code)。
使用 AnyDict 失去驗證 這類型別會跳過 Pydantic 的檢查。 盡量使用具體模型或 TypedDict,必要時自行在 @validator 中加入檢查。

最佳實踐

  1. 盡量在模型層完成驗證,避免在路由函式內手寫檢查。
  2. 為每個欄位加上說明 (description),OpenAPI 文件會自動呈現,提升 API 可讀性。
  3. 使用 @root_validator 處理跨欄位的商業規則(例如「開始日期必須早於結束日期」)。
  4. 在測試階段使用 TestClient 進行端到端驗證,確保 ValidationError 行為如預期。
  5. 統一錯誤回應格式,透過全局例外處理器或自訂 HTTPException,讓前端開發者只需要寫一次錯誤處理邏輯。

實際應用場景

1. 電子商務平台的訂單建立

  • 需求:使用者必須提供有效的收貨地址、商品清單、付款資訊。
  • 做法:定義 OrderCreateAddressPaymentInfo 等多層模型,讓 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 不會意外回傳錯誤或敏感資料。
  • 為了提升開發效率與使用者體驗,建議:
    1. 在模型層完成所有驗證,避免在路由裡重複撰寫檢查程式。
    2. 自訂錯誤訊息 讓前端更易於呈現。
    3. 使用全局例外處理器 統一錯誤回傳格式。
    4. 在測試階段使用 TestClient 確認驗證行為。

掌握了 ValidationError 的自動產生與客製化技巧後,你的 FastAPI 專案將會變得更安全、更易於維護,也能為前端團隊提供即時、結構化的錯誤回饋,提升整體開發效率。祝你在 FastAPI 的旅程中玩得開心、寫得順利! 🚀