FastAPI 教學:多重 Body(同時接收多個 Pydantic 模型)
簡介
在建構 API 時,Body 參數是最常見的資料來源之一。
FastAPI 透過 Pydantic 模型自動完成資料驗證與轉換,讓開發者只要專注於業務邏輯即可。然而,當一個端點需要同時接受 兩個或以上的 JSON 物件 時,直接在函式簽名中寫多個模型會出現「只能有一個 body」的限制。
本篇文章將說明 如何在 FastAPI 中處理多重 body,包含:
- 為什麼需要多個模型(常見情境)
- 核心概念與實作方式(
Body(..., embed=True)、Depends、自訂依賴等) - 多個實用範例與程式碼說明
- 常見陷阱與最佳實踐
- 真實應用案例
即使你是剛接觸 FastAPI 的新手,只要跟著步驟走,也能輕鬆在專案中加入多重 Body 的支援。
核心概念
1. FastAPI 僅允許 一個 直接的 Body 參數
FastAPI 會把函式簽名中 第一個 被視為 Body 的參數(非 Query、Path、Header、Cookie 等)當作整個請求的 JSON 內容。若同時寫入兩個模型,會拋出 Multiple body parameters are not allowed 的錯誤。
解法:把多個模型「包裝」成單一的 JSON 結構,或利用 依賴注入(Depends) 讓每個模型分別解析。
2. Body(..., embed=True):把模型嵌入外層 JSON
預設情況下,FastAPI 期望請求 Body 為 單一模型的純 JSON(例如 { "name": "Alice", "age": 30 })。若想在同一個請求中傳入多個模型,我們可以:
class User(BaseModel):
name: str
age: int
class Address(BaseModel):
city: str
zip_code: str
@app.post("/users/")
def create_user(
user: User = Body(..., embed=True),
address: Address = Body(..., embed=True)
):
...
此時,客戶端必須傳送:
{
"user": { "name": "Alice", "age": 30 },
"address": { "city": "Taipei", "zip_code": "100" }
}
重點:
embed=True讓 FastAPI 把模型包裝在外層鍵名(user、address)之下,從而同時接受多個 Body。
3. 使用 依賴注入(Depends) 解析多個模型
另一種更彈性的方式是把每個模型寫成 依賴,讓 FastAPI 分別解析它們,再把結果傳入最終的路由函式。
def get_user(user: User = Body(...)):
return user
def get_address(address: Address = Body(...)):
return address
@app.post("/users/")
def create_user(
user: User = Depends(get_user),
address: Address = Depends(get_address)
):
...
此寫法的好處是 可重用:同一個依賴可以在多個路由中共用,且可以在依賴內加入額外的前置驗證或資料轉換。
4. 以單一模型包裝多個子模型
如果你希望保持「單一 Body」的概念,也可以自行建立一個「容器」模型:
class UserCreateRequest(BaseModel):
user: User
address: Address
路由只接受 payload: UserCreateRequest,而 FastAPI 會自動遞迴驗證子模型。
程式碼範例
以下示範 5 個常見且實用的寫法,從最簡單到較進階的技巧都有涵蓋。
範例 1:最簡單的 embed=True 多重 Body
from fastapi import FastAPI, Body
from pydantic import BaseModel
app = FastAPI()
class User(BaseModel):
name: str
age: int
class Address(BaseModel):
city: str
zip_code: str
@app.post("/register/")
def register(
user: User = Body(..., embed=True),
address: Address = Body(..., embed=True)
):
"""
同時接收使用者資訊與地址資訊。
"""
return {"user": user, "address": address}
測試請求(使用 `curl)**
curl -X POST "http://localhost:8000/register/" \ -H "Content-Type: application/json" \ -d '{"user":{"name":"Bob","age":28},"address":{"city":"Kaohsiung","zip_code":"800"}}'
範例 2:使用 Depends 讓模型可重用
from fastapi import Depends, FastAPI
from pydantic import BaseModel
app = FastAPI()
class OrderItem(BaseModel):
product_id: int
quantity: int
class ShippingInfo(BaseModel):
address: str
method: str
def get_items(items: list[OrderItem] = Body(...)):
# 這裡可以加入額外的檢查,例如庫存驗證
return items
def get_shipping(info: ShippingInfo = Body(...)):
return info
@app.post("/orders/")
def create_order(
items: list[OrderItem] = Depends(get_items),
shipping: ShippingInfo = Depends(get_shipping)
):
return {"items": items, "shipping": shipping}
說明:
list[OrderItem]直接支援陣列型別。- 依賴函式
get_items、get_shipping可以分別加入業務邏輯,保持路由函式乾淨。
範例 3:單一容器模型的寫法(適合 Swagger 文件更清晰)
class Profile(BaseModel):
bio: str
avatar_url: str | None = None
class UserCreate(BaseModel):
user: User
profile: Profile
@app.post("/users/create")
def create_user(payload: UserCreate):
# payload.user 與 payload.profile 分別取得
return {"user": payload.user, "profile": payload.profile}
Swagger:在自動產生的 API 文件中,會顯示
user、profile兩個子欄位,讓前端開發者一眼就看懂結構。
範例 4:混合使用 Query、Path 與多重 Body
@app.put("/users/{user_id}")
def update_user(
user_id: int,
active: bool = Query(False),
user: User = Body(..., embed=True),
address: Address = Body(..., embed=True)
):
"""
- `user_id` 為路徑參數
- `active` 為查詢參數,用來切換使用者是否啟用
- 兩個 Body 分別負責更新資訊
"""
# 假設有 update_user_in_db 的函式
# update_user_in_db(user_id, user, address, active)
return {"id": user_id, "active": active, "user": user, "address": address}
重點:同時使用
Path、Query與多重 Body,FastAPI 會自動辨識每個來源的優先順序。
範例 5:自訂依賴結合驗證與轉換(進階實務)
from fastapi import HTTPException, status
def validate_user(user: User = Body(...)):
if user.age < 18:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="使用者年齡必須滿 18 歲"
)
return user
def validate_address(address: Address = Body(...)):
if not address.zip_code.isdigit():
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="郵遞區號只能是數字"
)
return address
@app.post("/secure-register/")
def secure_register(
user: User = Depends(validate_user),
address: Address = Depends(validate_address)
):
# 只要通過驗證,就可以安全寫入資料庫
return {"msg": "註冊成功", "user": user, "address": address}
技巧:把驗證邏輯寫在依賴裡,讓路由函式保持 單一職責(只負責業務流程),同時提升測試的可切割性。
常見陷阱與最佳實踐
| 陷阱 | 可能的結果 | 解決方式 |
|---|---|---|
直接在路由中寫多個模型 (未使用 embed=True 或 Depends) |
422 Unprocessable Entity 或 Multiple body parameters are not allowed |
使用 Body(..., embed=True)、容器模型或 Depends |
| 忘記在客戶端傳遞外層鍵名 | 解析失敗、返回 422 錯誤 | 確認 JSON 結構與 embed=True 的鍵名一致 |
| 模型名稱衝突 (兩個模型屬性同名) | Swagger 文件不清楚、驗證不正確 | 使用容器模型或在 Body(..., embed=True) 時自行指定別名 |
| 依賴函式過於複雜 | 測試困難、維護成本升高 | 只在依賴裡做「驗證」或「資料前處理」,保持簡潔 |
未設定 response_model |
回傳資料未經驗證,可能洩漏內部結構 | 為每個端點都設定 response_model,保持 API 穩定性 |
最佳實踐
- 優先使用容器模型:如果 API 的資料結構明確,將多個子模型包在一個
BaseModel裡,可讓 Swagger 文件更易讀。 - 利用
Depends重用驗證邏輯:把重複的驗證或前置處理抽成依賴,減少程式碼重複。 - 在
Body上使用embed=True:僅當前端已經固定傳遞外層鍵名時使用,否則容器模型更直觀。 - 為每個端點明確設定
response_model:保證回傳資料的型別安全,並自動產生文件。 - 測試多重 Body:使用
TestClient撰寫單元測試,確保 JSON 結構與模型驗證行為符合預期。
實際應用場景
1. 電商平台 – 建立訂單
- 需求:一次請求需要送出 商品清單(陣列)與 物流資訊(單一物件)。
- 解法:使用
Depends把OrderItem陣列與ShippingInfo分別解析,並在依賴內檢查庫存與物流可用性。
2. 社群網站 – 用戶註冊 + 首次個人檔案
- 需求:註冊時同時提供 帳號資訊 與 個人檔案(大頭貼、個人簡介)。
- 解法:建立
UserCreateRequest容器模型,讓前端一次傳送兩個子物件,後端一次驗證。
3. 金融系統 – 交易請求 + 風險評估參數
- 需求:交易資料與風險評估參數必須同時送出,且風險參數需經過額外驗證。
- 解法:使用
Depends把風險參數的驗證寫在獨立函式裡,保持交易路由的簡潔。
總結
- FastAPI 只允許一個直接的 Body,但透過
Body(..., embed=True)、容器模型 或 依賴注入(Depends),我們可以輕鬆在同一個端點中處理 多個 Pydantic 模型。 - 選擇哪種方式取決於 API 設計的可讀性、前端傳遞方式、以及 是否需要重用驗證邏輯。
- 最佳實踐:盡量使用容器模型或
Depends,配合response_model、完整的測試與清晰的 Swagger 文件,讓 API 更安全、易維護且對前端友好。
掌握了以上技巧後,你就能在 FastAPI 中自由組合各種 JSON 結構,為複雜的業務需求提供乾淨且可擴充的解決方案。祝開發順利 🎉!