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 文件,前端團隊能直接看到欄位說明與範例。
- 易於重用:同一個模型可以同時作為 Request 與 Response,或在不同路由間共享,減少重複程式碼。
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+)表示欄位可為null,gt=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。 |
最佳實踐
- 分層設計:
schemas.py只放 Pydantic 模型,models.py放 ORM 定義,crud.py處理資料庫操作。 - 使用
Config:啟用orm_mode、allow_population_by_field_name、use_enum_values(若使用 Enum)。 - 保持模型單一職責:Request Model 用於輸入驗證,Response Model 用於輸出,盡量不要混在一起。
- 加入說明文件:
Field(..., description="...")會自動顯現在 Swagger UI,提升 API 可讀性。 - 測試模型:利用
pydantic的.parse_obj()或.json()方法寫單元測試,確保驗證規則正確。
實際應用場景
使用者註冊與登入
UserRegister(Request)驗證 Email 與密碼。UserPublic(Response)只回傳id、username、email,避免暴露密碼雜湊。
電商商品 CRUD
ItemCreate、ItemUpdate分別處理建立與更新的欄位差異。ItemResponse結合orm_mode,直接回傳 SQLAlchemy 物件。
批次匯入資料
- 使用
List[ItemCreate]作為 Request Model,一次接受多筆商品資料,Pydantic 會為每筆自動驗證。
- 使用
多語系與國際化
- 透過
Enum定義語言代碼,並在模型裡使用language: LanguageEnum,配合use_enum_values = True讓 JSON 輸出為字串。
- 透過
文件上傳
- 雖然檔案本身不是 JSON,但可以在 Request Model 中加入
metadata: dict,描述檔案的額外資訊,讓前端一次傳送多種資料。
- 雖然檔案本身不是 JSON,但可以在 Request Model 中加入
總結
- Pydantic BaseModel 是 FastAPI 處理 資料驗證、序列化與文件產生 的核心工具。
- 透過
Field、validator、Config等功能,我們可以在 模型層 完成絕大多數的型別安全與基本業務檢查。 - 分離 Request / Response、使用內嵌模型、啟用
orm_mode,能讓 API 更具可讀性與安全性。 - 避免常見的陷阱(可變預設值、忘記
orm_mode、過度業務驗證),遵循 單一職責 與 層級分離 的最佳實踐,才能在大型專案中維持程式碼的可維護性。
掌握了 BaseModel 的寫法與最佳實踐後,你的 FastAPI 應用將能更快速地開發、減少錯誤、同時提供完整且易於使用的 API 文件。祝開發順利,期待看到你用 Pydantic 打造的高品質服務!