FastAPI 教學 – Pydantic 模型(Request / Response Models)
主題:Nested Model(巢狀模型)
簡介
在使用 FastAPI 建立 API 時,資料的驗證與序列化幾乎全靠 Pydantic 模型。當一筆請求或回應的結構變得複雜,常會出現「物件裡套物件」的情況,這時 Nested Model(巢狀模型)就派上用場。透過巢狀模型,我們可以:
- 清晰地描述層級資料,讓 IDE 能提供自動補完與型別檢查。
- 一次完成多層驗證,避免手動撰寫重複的檢查程式。
- 保持 API 文件的完整性,自動產生的 OpenAPI 會正確顯示每個子模型的結構。
本篇文章將從概念說明、實作範例、常見陷阱與最佳實踐,帶你一步步掌握 Pydantic 巢狀模型的使用方式,讓你的 FastAPI 專案更具可讀性與可靠性。
核心概念
1. 基本的巢狀模型寫法
Pydantic 允許在模型內部直接引用其他 BaseModel,形成層級結構。以下是一個最簡單的例子:
# file: models.py
from pydantic import BaseModel
from typing import List, Optional
class Address(BaseModel):
street: str
city: str
zip_code: str
class User(BaseModel):
id: int
name: str
email: str
address: Address # ← 巢狀模型
tags: Optional[List[str]] = None
User中的address欄位會自動套用Address的驗證規則。- 若傳入的 JSON 缺少
address中任何必填欄位,FastAPI 會回傳 422 Unprocessable Entity。
2. 在路由中使用巢狀模型
# file: main.py
from fastapi import FastAPI
from models import User
app = FastAPI()
@app.post("/users/")
async def create_user(user: User):
# 這裡的 user 已經是完整驗證過的 Python 物件
return {"msg": "User created", "user_id": user.id}
當客戶端送出以下 JSON 時,FastAPI 會自動把它轉成 User 實例:
{
"id": 1,
"name": "Alice",
"email": "alice@example.com",
"address": {
"street": "123 Main St",
"city": "Taipei",
"zip_code": "100"
},
"tags": ["admin", "tester"]
}
3. 巢狀模型的回應(Response Model)
from fastapi import FastAPI
from pydantic import BaseModel
from typing import List
class Product(BaseModel):
sku: str
name: str
price: float
class OrderItem(BaseModel):
product: Product
quantity: int
class OrderResponse(BaseModel):
order_id: int
items: List[OrderItem]
total: float
@app.get("/orders/{order_id}", response_model=OrderResponse)
async def get_order(order_id: int):
# 假設從資料庫取回的資料如下
order_data = {
"order_id": order_id,
"items": [
{
"product": {"sku": "A001", "name": "Keyboard", "price": 1999},
"quantity": 2
},
{
"product": {"sku": "B002", "name": "Mouse", "price": 799},
"quantity": 1
}
],
"total": 4797
}
return order_data
response_model會把回傳的 dict 轉換成OrderResponse,同時自動過濾掉未在模型中宣告的欄位。- 前端文件(Swagger UI)會清楚顯示
items陣列內每個OrderItem的結構。
4. 使用 Config 調整序列化行為
有時候我們希望在回傳時 排除 某些欄位(例如密碼或內部 ID),可以在子模型的 Config 裡設定:
class UserPublic(BaseModel):
id: int
name: str
email: str
class Config:
orm_mode = True # 允許從 ORM 物件直接建立模型
fields = {"email": {"exclude": True}} # 回傳時不顯示 email
當 UserPublic 作為 response_model 時,即使原始資料中有 email,Swagger UI 與實際回傳的 JSON 都不會包含它。
5. 進階:遞迴巢狀模型(樹狀結構)
若需要表示「父子」關係(如分類樹),可以使用 遞迴型別:
from __future__ import annotations
from typing import List, Optional
class Category(BaseModel):
id: int
name: str
parent_id: Optional[int] = None
children: List[Category] = [] # 自己引用自己
class Config:
orm_mode = True
FastAPI 會正確處理這種遞迴結構,前端取得的 JSON 會呈現巢狀的 children 陣列。
常見陷阱與最佳實踐
| 陷阱 | 可能的後果 | 解決方案 |
|---|---|---|
忘記在子模型加上 from __future__ import annotations(遞迴模型) |
產生 NameError: name 'Category' is not defined |
在檔案最前面加入 from __future__ import annotations,或使用字串前向引用 List["Category"] |
在 response_model 中使用可變預設值(list / dict) |
回傳的資料會被共享,導致跨請求的資料污染 | 使用 Field(default_factory=list) 來產生獨立的預設值 |
| 過度嵌套導致 Swagger UI 渲染緩慢 | 文件載入時間變長,使用者體驗下降 | 只在需要時使用巢狀模型,對於深層結構可考慮拆分為多個端點或使用 include_in_schema=False |
把 ORM 物件直接傳給 response_model 卻未開啟 orm_mode |
回傳時會拋出 ValueError: object has no attribute ... |
在子模型的 Config 加上 orm_mode = True |
未使用 typing.Optional 處理可為 null 的欄位 |
請求缺少該欄位時會被視為錯誤 | 明確宣告 Optional[Type] = None,或使用 Field(..., nullable=True) |
最佳實踐
- 模型分層:將共用子模型抽離成獨立檔案(例如
schemas/address.py),保持程式碼乾淨且易於維護。 - 使用
Field設定描述與範例:有助於自動產生的 API 文件更具可讀性。from pydantic import Field class Address(BaseModel): street: str = Field(..., description="街道名稱", example="中正路 1 號") city: str = Field(..., description="城市", example="台北市") zip_code: str = Field(..., regex=r'^\d{3}$', description="郵遞區號") - 避免過度暴露內部欄位:使用
exclude、include或建立「公開」版本的模型(如UserPublic)來控制回傳資料。 - 測試模型驗證:利用
pydantic的.parse_obj()或 FastAPI 的測試客戶端,確保不同層級的資料都能正確驗證。 - 記得
orm_mode:若你使用 SQLAlchemy、Tortoise ORM 等 ORM,務必在每個回傳模型的Config中開啟orm_mode,否則會出現屬性取得錯誤。
實際應用場景
1. 電商平台的訂單 API
- 請求模型:客戶端送出訂單時,需要同時傳遞「商品資訊」與「收貨地址」兩層結構。
- 回應模型:服務端回傳訂單編號、每筆商品的詳細資訊、計算好的總金額以及物流狀態。
使用巢狀模型可以一次驗證商品 SKU、數量、價格是否合理,同時檢查地址欄位的完整性,減少後端的防禦性程式碼。
2. 社交平台的貼文與留言
貼文 (Post) 內可能含有多筆「留言」(Comment),而每筆留言又可能有「回覆」(Reply)。遞迴模型讓我們只需要定義一次 Comment,即可支援任意深度的回覆樹。
3. 企業內部的組織圖 API
組織結構通常是「部門」包含「子部門」的樹狀結構。透過遞迴 Category(或 Department)模型,前端一次請求即可取得完整的階層圖,節省多次 API 呼叫。
總結
- 巢狀模型 是 Pydantic 與 FastAPI 的核心功能之一,讓我們能在單一模型中描述多層次資料結構。
- 透過
BaseModel相互引用、response_model、以及Config.orm_mode,可以輕鬆完成請求驗證與回應序列化。 - 注意 遞迴引用、可變預設值、資料隱私 等常見陷阱,遵守 分層、描述、測試 的最佳實踐,能讓 API 更安全、文件更易讀、維護成本更低。
- 在電商、社交、組織圖等實務場景中,巢狀模型不僅提升開發效率,也確保資料的一致性與正確性。
掌握了這些概念與技巧,你就能在 FastAPI 專案中自如地處理任何複雜的 JSON 結構,為使用者提供可靠且易於理解的 API 介面。祝開發順利,玩得開心!