FastAPI Request Body 巢狀模型與複雜資料結構 (Nested Models & Complex Data Structures)

隨著 API 功能越來越複雜,你傳輸的 JSON 資料結構往往不會只是扁平的鍵值對。你可能會有物件裡面包物件(巢狀結構),或是由物件組成的列表 (List of Objects)。

FastAPI 透過 Pydantic 強大的型別系統,可以輕鬆處理任意深度的複雜資料結構。

List 欄位 (列表)

要定義一個欄位為列表,可以使用 Python 標準庫 typing 中的 List (Python 3.9+ 可以直接用 list,但為了相容性與明確性,範例使用 List)。

from typing import Annotated
from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

class Item(BaseModel):
    name: str
    description: str | None = None
    price: float
    tax: float | None = None
    # tags 是一個字串列表,預設為空列表
    tags: list[str] = []

@app.post("/items/")
async def create_item(item: Item):
    return item
在 Python 3.9+ 中,你可以直接使用內建的 list[str] 而不需要從 typing 導入 List。同樣地,Python 3.10+ 支援使用 | None 來取代 Optional,這讓程式碼更加簡潔。

Request Body 範例:

{
  "name": "Foo",
  "price": 42.0,
  "tags": ["rock", "metal", "bar"]
}

Set 型別 (集合)

如果你希望列表中的元素是不重複的,可以使用 Set

class Item(BaseModel):
    name: str
    price: float
    tags: Set[str] = set()

如果傳入 ["foo", "bar", "foo"],FastAPI (Pydantic) 會自動去重,變成 {"foo", "bar"}

巢狀模型 (Nested Models)

你可以將一個 Pydantic 模型用作另一個模型的欄位型別。

from typing import Optional
from pydantic import BaseModel

class Image(BaseModel):
    url: str
    name: str

class Item(BaseModel):
    name: str
    description: Optional[str] = None
    price: float
    tags: Set[str] = set()
    # image 欄位本身就是一個 Image 物件
    image: Optional[Image] = None

@app.put("/items/{item_id}")
async def update_item(item_id: int, item: Item):
    results = {"item_id": item_id, "item": item}
    return results

Request Body 範例:

{
  "name": "Foo",
  "price": 35.4,
  "tags": ["offer"],
  "image": {
    "url": "http://example.com/baz.jpg",
    "name": "The Foo live"
  }
}

FastAPI 能夠自動解析並驗證內部的 image 物件是否符合 Image 模型的定義。

特殊型別與驗證

Pydantic 支援許多特殊的複雜型別,例如 URL 驗證。

from pydantic import BaseModel, HttpUrl

class Image(BaseModel):
    url: HttpUrl # 自動驗證是否為合法的 HTTP/HTTPS URL
    name: str

如果你傳入 "url": "not a url",會直接報錯,拒絕請求。

含有巢狀模型的列表

你可以結合 List 和 Model,定義一個「物件列表」。

class Item(BaseModel):
    name: str
    price: float
    # images 是一個 Image 物件的列表
    images: List[Image] = []

Request Body 範例:

{
  "name": "Foo",
  "price": 32.5,
  "images": [
    {
      "url": "http://example.com/baz.jpg",
      "name": "The Foo live"
    },
    {
      "url": "http://example.com/dave.jpg",
      "name": "The Baz"
    }
  ]
}

深層巢狀結構

你可以定義任意深度的巢狀結構:

class Offer(BaseModel):
    name: str
    description: Optional[str] = None
    price: float
    items: List[Item] # Offer 包含多個 Item,Item 又包含多個 Image

@app.post("/offers/")
async def create_offer(offer: Offer):
    return offer

Dict 型別 (字典)

如果你不知道鍵 (Key) 的名稱,但知道值 (Value) 的型別,可以使用 Dict

from typing import Dict

class Item(BaseModel):
    name: str
    # weights 是一個字典,Key 是 int,Value 是 float
    weights: Dict[int, float]

Request Body 範例:

{
  "name": "Foo",
  "weights": {
    "1": 0.5,
    "2": 0.3
  }
}

注意:JSON 的 Key 永遠是字串,但因為我們定義 Dict[int, float],Pydantic 會嘗試將 Key "1" 轉為整數 1

總結

  • 利用 List, Set, Dict 等標準型別宣告複雜結構。
  • 將 Pydantic Model 作為欄位型別,即可建立巢狀模型。
  • 可以組合使用,例如 List[YourModel]
  • Pydantic 會自動遞迴地驗證所有深層資料。