本文 AI 產出,尚未審核

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_idquantity

範例 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 效能下降 依功能劃分模型,僅在必要的路由引用,保持檔案模組化

最佳實踐

  1. 模型拆分:將每個概念(地址、商品、設定)抽成獨立的 BaseModel,提升重用性。
  2. 使用 Config
    class Config:
        orm_mode = True                 # 與 ORM 互通
        allow_population_by_field_name = True
    
  3. 自訂驗證:在模型內加入 @validator,在資料不符合業務規則時拋出明確錯誤。
  4. 文件化:利用 FastAPI 自動產生的 OpenAPI,確保每個欄位都有說明 (description) 與範例 (example)。
  5. 測試:使用 TestClient 撰寫單元測試,驗證巢狀結構的正確性與錯誤回應。

實際應用場景

場景 為何需要多層巢狀 Body 範例模型
電商平台的訂單 API 包含客戶、收貨地址、商品清單、每商品的規格與庫存 OrderCustomerOrderItemItemOption
社交平台的貼文發佈 貼文本身、作者資訊、附件(圖片、影片)與標籤列表 PostAuthorAttachmentTag
企業內部的設定管理 系統設定、使用者偏好、通知方式多層次配置 SystemConfigUserPreferenceNotificationSetting
物流系統的批次出貨 多筆出貨單、每筆包含多個貨品與每個貨品的倉庫分配 BatchShipmentShipmentItemWarehouseAllocation

在上述情境中,多層巢狀 Body 能讓 API 的請求結構直接映射到業務概念,減少前端與後端之間的轉換成本,同時利用 Pydantic 的型別檢查避免錯誤資料進入系統。


總結

  • 多層巢狀 Body 是 FastAPI 處理複雜 JSON 請求的核心能力,透過 Pydantic 模型的巢狀與列表組合,可完整描述實務上常見的資料結構。
  • 透過 alias、預設值與 Optional,可以彈性對應不同前端需求,同時保持資料驗證的嚴謹性。
  • 避免常見陷阱(欄位名稱不符、缺少預設值、模型過於龐大)並遵循 最佳實踐(模型拆分、Config 設定、單元測試),能讓 API 更健壯、易於維護。
  • 在電商、社交、設定管理、物流等多種應用場景中,多層巢狀 Body 已成為不可或缺的設計模式。

掌握了本文的概念與範例後,你就能在 FastAPI 專案中自信地設計出 結構清晰、驗證完整、易於擴充 的 API,為後續的功能迭代與團隊協作奠定堅實基礎。祝開發順利 🚀!