巢狀模型
巢狀模型(nested models)是把一個 Pydantic 模型當作另一個模型的欄位型別,用來描述多層的 JSON 結構。Pydantic 會遞迴驗證整棵資料樹,錯誤訊息會附上完整路徑。本頁介紹基本巢狀、模型串列、自我參照的遞迴結構,以及用 Discriminated Union 依欄位值選擇不同模型。
如何讓模型包含另一個模型?
直接把模型當作型別使用即可,傳入 dict 會自動轉成對應的模型物件:
from pydantic import BaseModel
class Address(BaseModel):
city: str
street: str
class Item(BaseModel):
name: str
price: float
quantity: int = 1
class Order(BaseModel):
id: int
shipping: Address
items: list[Item]
@property
def total(self) -> float:
return sum(i.price * i.quantity for i in self.items)
data = {
"id": 1001,
"shipping": {"city": "台北市", "street": "忠孝東路一段 1 號"},
"items": [
{"name": "鍵盤", "price": 1290},
{"name": "滑鼠", "price": 590, "quantity": 2},
],
}
order = Order.model_validate(data)
print(order.shipping.city) # 台北市
print(order.items[1].name) # 滑鼠
print(order.total) # 2470.0
model_dump() 也會遞迴把所有子模型轉回 dict。
巢狀資料出錯時,怎麼知道是哪一筆?
錯誤的 loc 會包含完整路徑,串列會顯示索引:
from pydantic import BaseModel, ValidationError
class Item(BaseModel):
name: str
price: float
class Cart(BaseModel):
items: list[Item]
try:
Cart.model_validate({"items": [{"name": "A", "price": 10}, {"name": "B", "price": "免費"}]})
except ValidationError as e:
print(e.errors()[0]["loc"]) # ('items', 1, 'price')
讀法:items 串列的第 2 筆(索引 1)的 price 欄位有問題。
樹狀或遞迴的結構要怎麼定義?
模型可以參照自己,例如目錄樹或留言串。用字串寫型別名稱(前向參照,forward reference)即可:
from pydantic import BaseModel
class Comment(BaseModel):
author: str
text: str
replies: list["Comment"] = []
thread = Comment.model_validate({
"author": "A",
"text": "Pydantic 好用嗎?",
"replies": [{"author": "B", "text": "很好用", "replies": [{"author": "A", "text": "感謝"}]}],
})
print(thread.replies[0].replies[0].text) # 感謝
一個欄位可能是不同模型時怎麼辦?
例如付款方式可能是信用卡或轉帳,兩者欄位不同。最好的做法是加一個 Literal 標記欄位,並用 Field(discriminator=...) 告訴 Pydantic 依它選模型。這叫 Discriminated Union(帶標記的聯集),比讓 Pydantic 逐一嘗試更快、錯誤訊息也更清楚:
from typing import Literal
from pydantic import BaseModel, Field
class CreditCard(BaseModel):
method: Literal["card"]
card_number: str
class BankTransfer(BaseModel):
method: Literal["transfer"]
bank_code: str
account: str
class Payment(BaseModel):
amount: float
detail: CreditCard | BankTransfer = Field(discriminator="method")
p = Payment.model_validate({"amount": 500, "detail": {"method": "transfer", "bank_code": "004", "account": "123"}})
print(type(p.detail).__name__) # BankTransfer
推薦影音
James Clare:Nested Models & Complex JSON
簡述:約 11 分鐘,示範如何用巢狀模型表示複雜的 JSON 結構,適合想練習「把 API 回應轉成模型」的讀者。巢狀模型在 FastAPI 中的用法,可再看 FastAPI 整合 頁的 BugBytes 影片。