FastAPI 教學:多層巢狀 Body 參數處理
簡介
在開發 RESTful API 時,請求的 Body 常常不只是一個簡單的鍵值對,而是包含多層結構的 JSON。
例如,建立一筆訂單時,需要同時傳遞「客戶資訊」→「地址」以及「商品清單」→「每項商品的規格」等資料。
如果只用單層的 Pydantic 模型來描述,會讓程式碼變得雜亂且難以維護;而 多層巢狀 Body 則能讓資料結構與實際需求保持一一對應,提升可讀性與型別安全。
本篇文章將從 概念說明、實作範例、常見陷阱、最佳實踐 以及 實務應用 四個面向,完整介紹在 FastAPI 中如何處理多層巢狀的請求 Body,讓你能快速寫出結構清晰、易於擴充的 API。
核心概念
1. Pydantic 模型的巢狀結構
FastAPI 以 Pydantic 作為資料驗證與序列化的核心。
只要把一個 BaseModel 當作另一個模型的屬性,就能形成巢狀結構:
from pydantic import BaseModel, EmailStr
from typing import List, Optional
class Address(BaseModel):
street: str
city: str
zip_code: str
class Customer(BaseModel):
name: str
email: EmailStr
address: Address # <─ 巢狀模型
Customer 內部的 address 欄位會自動套用 Address 的驗證規則,若傳入的 JSON 不符合結構,FastAPI 會回傳 422 Unprocessable Entity。
2. List 與 List[BaseModel] 的結合
多筆資料(如購物車商品)通常以陣列形式呈現。只要把 List[YourModel] 放在父模型裡,即可完成多層陣列的驗證:
class ItemOption(BaseModel):
option_name: str
option_value: str
class OrderItem(BaseModel):
product_id: int
quantity: int
options: List[ItemOption] = [] # 可為空的巢狀陣列
class Order(BaseModel):
order_id: str
customer: Customer
items: List[OrderItem] # 多筆商品
3. Optional 與預設值
在實務上,某些欄位可能不是必填,這時可以使用 Optional 或直接給予 預設值:
class PaymentInfo(BaseModel):
method: str
transaction_id: Optional[str] = None # 可能在付款完成後才回傳
4. 使用 Body 參數自訂名稱
如果前端傳遞的 JSON 欄位名稱與 Python 變數不一致,Field(..., alias="json_name") 可以解決:
class Address(BaseModel):
street: str = Field(..., alias="street_name")
city: str
zip_code: str = Field(..., alias="postal_code")
FastAPI 會自動根據 alias 解析傳入的 JSON,並在回傳時仍使用模型的屬性名稱(除非設定 by_alias=True)。
程式碼範例
以下示範 3 個實務常見的多層巢狀 Body,每個範例都包含完整的路由與說明。
範例 1️⃣:建立訂單(含客戶資訊、地址、商品與規格)
# file: main.py
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, EmailStr, Field
from typing import List, Optional
app = FastAPI()
class Address(BaseModel):
street: str = Field(..., alias="street_name")
city: str
zip_code: str = Field(..., alias="postal_code")
class Customer(BaseModel):
name: str
email: EmailStr
address: Address
class ItemOption(BaseModel):
option_name: str
option_value: str
class OrderItem(BaseModel):
product_id: int
quantity: int = Field(..., gt=0, description="必須大於 0")
options: List[ItemOption] = []
class Order(BaseModel):
order_id: str
customer: Customer
items: List[OrderItem]
notes: Optional[str] = None
@app.post("/orders")
async def create_order(order: Order):
"""
接收多層巢狀的訂單資料,若驗證通過則回傳成功訊息。
"""
# 這裡通常會把資料寫入資料庫
return {"message": "訂單建立成功", "order_id": order.order_id}
說明
Address使用alias處理前端不同的欄位名稱。OrderItem.quantity透過gt=0限制只能是正整數。options預設為空陣列,讓前端可以只傳product_id與quantity。
範例 2️⃣:使用者註冊(含多層設定與偏好)
from fastapi import FastAPI
from pydantic import BaseModel, EmailStr, validator
from typing import List, Dict
app = FastAPI()
class NotificationSetting(BaseModel):
email: bool = True
sms: bool = False
class Preference(BaseModel):
theme: str = "light" # light / dark
language: str = "zh-TW"
notifications: NotificationSetting
class RegisterUser(BaseModel):
username: str
password: str
email: EmailStr
profile: Dict[str, str] = {} # 任意鍵值對,如 {"bio": "..."}
preferences: Preference
@router.post("/register")
async def register(user: RegisterUser):
# 密碼雜湊、寄送驗證信等
return {"msg": f"使用者 {user.username} 註冊成功"}
說明
preferences內部再巢狀NotificationSetting,讓 UI 可以一次開關所有通知方式。profile使用Dict[str, str],允許前端自行加入任意自訂欄位。
範例 3️⃣:批次更新商品資訊(含多層陣列與可選欄位)
from fastapi import FastAPI, Body
from pydantic import BaseModel, PositiveInt, conint
from typing import List, Optional
app = FastAPI()
class StockUpdate(BaseModel):
warehouse_id: int
quantity: conint(ge=0) # 必須 >= 0
class ProductUpdate(BaseModel):
product_id: PositiveInt
price: Optional[float] = None # 若不提供則不變更
stocks: List[StockUpdate] = [] # 可一次更新多個倉庫的庫存
@app.put("/products/batch")
async def batch_update(updates: List[ProductUpdate] = Body(..., embed=True)):
"""
接收一個產品更新清單,每筆資料可能包含價格與多筆庫存變更。
"""
# 迭代處理每筆更新
for upd in updates:
# 這裡寫入 DB 或呼叫其他服務
pass
return {"status": "batch update completed", "count": len(updates)}
說明
embed=True讓請求的 JSON 必須是{"updates": [...]}的形式,避免與其他參數衝突。conint(ge=0)確保庫存量不會是負數。price為可選欄位,若前端不傳則保持原值。
常見陷阱與最佳實踐
| 陷阱 | 可能的結果 | 解決方案 |
|---|---|---|
忘記在巢狀模型中使用 Field(..., alias=…) |
前端傳的欄位名稱不匹配,導致 422 錯誤 | 依需求使用 alias,或在 Config 中設定 allow_population_by_field_name = True |
| 列表欄位未設定預設值 | 若前端不傳該欄位,FastAPI 會回傳 422 | 為 List 欄位加上 = [] 或 Optional[List[...]] = None |
| 過度深層的模型 | 可讀性下降、維護成本升高 | 盡量把共用子模型抽離成獨立 BaseModel,或使用 Union/Any 處理彈性需求 |
| 模型驗證錯誤訊息不友好 | 前端不易定位問題 | 使用 @validator 加上自訂錯誤訊息,或在 FastAPI 設定 responses 給予說明 |
| 大型 JSON 直接映射到單一模型 | 產生巨大的模型檔,導致 IDE 效能下降 | 依功能劃分模型,僅在必要的路由引用,保持檔案模組化 |
最佳實踐
- 模型拆分:將每個概念(地址、商品、設定)抽成獨立的
BaseModel,提升重用性。 - 使用
Config:class Config: orm_mode = True # 與 ORM 互通 allow_population_by_field_name = True - 自訂驗證:在模型內加入
@validator,在資料不符合業務規則時拋出明確錯誤。 - 文件化:利用 FastAPI 自動產生的 OpenAPI,確保每個欄位都有說明 (
description) 與範例 (example)。 - 測試:使用
TestClient撰寫單元測試,驗證巢狀結構的正確性與錯誤回應。
實際應用場景
| 場景 | 為何需要多層巢狀 Body | 範例模型 |
|---|---|---|
| 電商平台的訂單 API | 包含客戶、收貨地址、商品清單、每商品的規格與庫存 | Order、Customer、OrderItem、ItemOption |
| 社交平台的貼文發佈 | 貼文本身、作者資訊、附件(圖片、影片)與標籤列表 | Post、Author、Attachment、Tag |
| 企業內部的設定管理 | 系統設定、使用者偏好、通知方式多層次配置 | SystemConfig、UserPreference、NotificationSetting |
| 物流系統的批次出貨 | 多筆出貨單、每筆包含多個貨品與每個貨品的倉庫分配 | BatchShipment、ShipmentItem、WarehouseAllocation |
在上述情境中,多層巢狀 Body 能讓 API 的請求結構直接映射到業務概念,減少前端與後端之間的轉換成本,同時利用 Pydantic 的型別檢查避免錯誤資料進入系統。
總結
- 多層巢狀 Body 是 FastAPI 處理複雜 JSON 請求的核心能力,透過 Pydantic 模型的巢狀與列表組合,可完整描述實務上常見的資料結構。
- 透過 alias、預設值與 Optional,可以彈性對應不同前端需求,同時保持資料驗證的嚴謹性。
- 避免常見陷阱(欄位名稱不符、缺少預設值、模型過於龐大)並遵循 最佳實踐(模型拆分、Config 設定、單元測試),能讓 API 更健壯、易於維護。
- 在電商、社交、設定管理、物流等多種應用場景中,多層巢狀 Body 已成為不可或缺的設計模式。
掌握了本文的概念與範例後,你就能在 FastAPI 專案中自信地設計出 結構清晰、驗證完整、易於擴充 的 API,為後續的功能迭代與團隊協作奠定堅實基礎。祝開發順利 🚀!