本文 AI 產出,尚未審核

FastAPI – Pydantic 模型(Request / Response Models)

建立 Pydantic BaseModel


簡介

FastAPI 中,資料驗證與序列化的核心工具就是 Pydantic。無論是接收前端送來的請求資料,或是回傳給前端的 JSON,都建議使用 BaseModel 來描述結構。這樣不僅能自動完成型別檢查、錯誤回報,還能讓 IDE 提供完整的自動補完,提升開發效率與程式可讀性。

本篇文章將從 什麼是 Pydantic BaseModel如何定義欄位與驗證規則,到 實務上常見的陷阱與最佳實踐,一步步帶你建立可靠的 Request / Response 模型,讓你的 FastAPI 專案更安全、更易維護。


核心概念

1. 為什麼要用 BaseModel?

  • 型別安全:在請求進入路由前,FastAPI 會自動把 JSON 轉成 BaseModel 實例,若型別不符會拋出 422 錯誤。
  • 自動文件:FastAPI 會根據模型自動生成 OpenAPI 文件,前端團隊能直接看到欄位說明與範例。
  • 易於重用:同一個模型可以同時作為 RequestResponse,或在不同路由間共享,減少重複程式碼。

2. 基本語法

from pydantic import BaseModel, Field

class UserCreate(BaseModel):
    username: str = Field(..., max_length=20, description="使用者名稱")
    email: str = Field(..., regex=r'^\S+@\S+\.\S+$', description="電子郵件")
    password: str = Field(..., min_length=8, description="密碼")
  • ... 表示 必填
  • Field 可以設定 限制條件、預設值、說明文字,這些資訊會被 FastAPI 直接顯示在 API 文件中。

3. 欄位型別與進階驗證

欄位型別 說明 範例
str 文字 name: str
int 整數 age: int = 0
float 浮點數 price: float
bool 布林值 is_active: bool = True
datetime 日期時間 created_at: datetime
list[T] 陣列 tags: List[str] = []
Optional[T] 可為 None nickname: Optional[str] = None

Pydantic 內建許多 驗證器(validator),可以在欄位值被賦值前自訂檢查邏輯。

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

4. 內嵌模型(Nested Model)

在實務上,資料往往是多層結構。只要把其他 BaseModel 當作欄位型別即可。

class Address(BaseModel):
    city: str
    street: str
    zip_code: str

class User(BaseModel):
    username: str
    address: Address

5. 讀寫分離的模型

為了避免在回傳資料時洩漏敏感欄位(例如密碼),可以 繼承 原始模型,並 排除 不需要的欄位。

class UserInDB(UserCreate):
    id: int
    hashed_password: str

class UserResponse(UserInDB):
    class Config:
        orm_mode = True
        fields = {
            'hashed_password': {'exclude': True},
            'password': {'exclude': True},
        }

程式碼範例

以下提供 5 個實用範例,涵蓋從最簡單的模型到較為進階的使用情境。每段程式碼皆附上說明,方便您直接套用到自己的專案。

範例 1️⃣ 基本的 Request Model

# file: schemas.py
from pydantic import BaseModel, Field

class ItemCreate(BaseModel):
    """建立商品的請求模型"""
    name: str = Field(..., max_length=50, description="商品名稱")
    description: str | None = Field(None, description="商品描述")
    price: float = Field(..., gt=0, description="商品單價,必須大於 0")

說明:使用 | None(Python 3.10+)表示欄位可為 nullgt=0 代表「大於 0」。

範例 2️⃣ 回傳模型(Response Model)與 ORM Mode

# file: schemas.py
from pydantic import BaseModel

class ItemResponse(BaseModel):
    """回傳給前端的商品資訊"""
    id: int
    name: str
    description: str | None
    price: float

    class Config:
        orm_mode = True   # 讓 FastAPI 能直接接受 SQLAlchemy ORM 物件

重點orm_mode = True 讓 Pydantic 能從 ORM 物件自動取值,避免手動轉換。

範例 3️⃣ 內嵌模型(Nested Model)

# file: schemas.py
from typing import List
from pydantic import BaseModel

class Tag(BaseModel):
    name: str

class BlogPostCreate(BaseModel):
    title: str
    content: str
    tags: List[Tag] = []   # 預設空清單,允許多個標籤

實務應用:建立部落格文章時,同時送出多個標籤,只要把 Tag 放進 tags 陣列即可。

範例 4️⃣ 自訂驗證器(Validator)

# file: schemas.py
from pydantic import BaseModel, validator
import re

class UserRegister(BaseModel):
    email: str
    password: str

    @validator('email')
    def email_must_be_valid(cls, v):
        if not re.match(r'^\S+@\S+\.\S+$', v):
            raise ValueError('無效的 Email 格式')
        return v

    @validator('password')
    def password_complexity(cls, v):
        # 至少 8 個字元,且必須同時包含大小寫與數字
        if len(v) < 8 or not re.search(r'[A-Z]', v) \
           or not re.search(r'[a-z]', v) or not re.search(r'\d', v):
            raise ValueError('密碼需包含大小寫字母與數字,且長度至少 8')
        return v

技巧validator 可以同時檢查多個欄位,只要在 @validator('field1', 'field2') 裡列出欄位名稱。

範例 5️⃣ 讀寫分離的模型(隱藏敏感資訊)

# file: schemas.py
from pydantic import BaseModel, Field

class UserInDB(BaseModel):
    id: int
    username: str
    email: str
    hashed_password: str = Field(..., description="已加密的密碼")

    class Config:
        orm_mode = True

class UserPublic(BaseModel):
    id: int
    username: str
    email: str

    class Config:
        orm_mode = True

在路由中:

@app.get("/users/{user_id}", response_model=UserPublic)
def get_user(user_id: int, db: Session = Depends(get_db)):
    db_user = db.query(UserModel).filter(UserModel.id == user_id).first()
    return db_user   # FastAPI 只會回傳 UserPublic 定義的欄位

關鍵:透過 response_model 指定公開模型,避免把 hashed_password 直接暴露給前端。


常見陷阱與最佳實踐

陷阱 說明 解決方式
忘記設定 orm_mode 使用 SQLAlchemy 時,直接回傳 ORM 物件會出現 value is not a valid dict 錯誤。 在模型的 Config 中加入 orm_mode = True
使用可變預設值 (list = []) Pydantic 會把同一個物件共享給所有實例,導致資料污染。 使用 Field(default_factory=list) 或直接在型別註解中給予 list
過度驗證 在模型內寫太多業務邏輯驗證,會讓模型變得難以維護。 業務規則 放到服務層或依賴注入的函式中,模型只負責基礎型別驗證。
未使用 Optional 欄位若允許 null,卻未宣告為 Optional,會導致 422 錯誤。 Optional[T] = None 明確表示可為 None
忽略 alias 前端傳來的 JSON 鍵名與 Python 變數不一致時會失敗。 使用 Field(..., alias="jsonKey") 並在 Config 設定 allow_population_by_field_name = True

最佳實踐

  1. 分層設計schemas.py 只放 Pydantic 模型models.py 放 ORM 定義,crud.py 處理資料庫操作。
  2. 使用 Config:啟用 orm_modeallow_population_by_field_nameuse_enum_values(若使用 Enum)。
  3. 保持模型單一職責:Request Model 用於輸入驗證,Response Model 用於輸出,盡量不要混在一起。
  4. 加入說明文件Field(..., description="...") 會自動顯現在 Swagger UI,提升 API 可讀性。
  5. 測試模型:利用 pydantic.parse_obj().json() 方法寫單元測試,確保驗證規則正確。

實際應用場景

  1. 使用者註冊與登入

    • UserRegister(Request)驗證 Email 與密碼。
    • UserPublic(Response)只回傳 id、username、email,避免暴露密碼雜湊。
  2. 電商商品 CRUD

    • ItemCreateItemUpdate 分別處理建立與更新的欄位差異。
    • ItemResponse 結合 orm_mode,直接回傳 SQLAlchemy 物件。
  3. 批次匯入資料

    • 使用 List[ItemCreate] 作為 Request Model,一次接受多筆商品資料,Pydantic 會為每筆自動驗證。
  4. 多語系與國際化

    • 透過 Enum 定義語言代碼,並在模型裡使用 language: LanguageEnum,配合 use_enum_values = True 讓 JSON 輸出為字串。
  5. 文件上傳

    • 雖然檔案本身不是 JSON,但可以在 Request Model 中加入 metadata: dict,描述檔案的額外資訊,讓前端一次傳送多種資料。

總結

  • Pydantic BaseModel 是 FastAPI 處理 資料驗證、序列化與文件產生 的核心工具。
  • 透過 FieldvalidatorConfig 等功能,我們可以在 模型層 完成絕大多數的型別安全與基本業務檢查。
  • 分離 Request / Response使用內嵌模型啟用 orm_mode,能讓 API 更具可讀性與安全性。
  • 避免常見的陷阱(可變預設值、忘記 orm_mode、過度業務驗證),遵循 單一職責層級分離 的最佳實踐,才能在大型專案中維持程式碼的可維護性。

掌握了 BaseModel 的寫法與最佳實踐後,你的 FastAPI 應用將能更快速地開發、減少錯誤、同時提供完整且易於使用的 API 文件。祝開發順利,期待看到你用 Pydantic 打造的高品質服務!