本文 AI 產出,尚未審核

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)

最佳實踐

  1. 模型分層:將共用子模型抽離成獨立檔案(例如 schemas/address.py),保持程式碼乾淨且易於維護。
  2. 使用 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="郵遞區號")
    
  3. 避免過度暴露內部欄位:使用 excludeinclude 或建立「公開」版本的模型(如 UserPublic)來控制回傳資料。
  4. 測試模型驗證:利用 pydantic.parse_obj() 或 FastAPI 的測試客戶端,確保不同層級的資料都能正確驗證。
  5. 記得 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 介面。祝開發順利,玩得開心!