FastAPI 教學:Pydantic 模型的 Config 設定(orm_mode、json_encoders)
簡介
在使用 FastAPI 開發 API 時,Pydantic 是負責資料驗證與序列化的核心工具。除了宣告欄位型別外,Pydantic 也提供了 Config 類別讓我們客製化模型的行為。其中最常用的兩個設定是 orm_mode 與 json_encoders,它們分別解決了「如何直接使用 ORM 物件」以及「如何自訂 JSON 序列化」的問題。
為什麼需要
orm_mode?
當我們的資料來源是 SQLAlchemy、Tortoise‑ORM 等 ORM 時,模型通常是 Python 物件而非字典。若不啟用orm_mode,FastAPI 會在回傳時拋出錯誤,因為 Pydantic 預設只接受dict或BaseModel。為什麼需要
json_encoders?
有些資料型別(例如datetime、Decimal、UUID)在預設的 JSON 序列化器中不被支援,會導致TypeError: Object of type ... is not JSON serializable。透過json_encoders,我們可以告訴 Pydantic 如何把這些型別轉成 JSON 可接受的格式。
本篇文章將從概念說明、實作範例、常見陷阱與最佳實踐,最後帶你了解實務應用的情境,幫助你在 FastAPI 專案中更得心應手地使用 Config。
核心概念
1. orm_mode:讓 Pydantic 能直接讀取 ORM 物件
為什麼要用?
- ORM 物件的屬性是以屬性存取(
user.id、user.name),而非字典鍵值(user["id"])。 - 啟用
orm_mode後,Pydantic 會把模型視為「類似 dict」的資料來源,自動呼叫getattr取得屬性值。
基本範例
# models.py(SQLAlchemy 範例)
from sqlalchemy import Column, Integer, String
from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
username = Column(String, unique=True, index=True)
email = Column(String, unique=True, index=True)
# schemas.py(Pydantic 模型)
from pydantic import BaseModel
class UserSchema(BaseModel):
id: int
username: str
email: str
class Config:
orm_mode = True # <--- 這裡啟用 orm_mode
# main.py(FastAPI 路由)
from fastapi import FastAPI, Depends
from sqlalchemy.orm import Session
from . import models, schemas, crud, database
app = FastAPI()
@app.get("/users/{user_id}", response_model=schemas.UserSchema)
def read_user(user_id: int, db: Session = Depends(database.get_db)):
db_user = crud.get_user(db, user_id) # 回傳的是 SQLAlchemy User 物件
return db_user # 直接返回,Pydantic 會自動轉換
重點:只要在
UserSchema.Config中設定orm_mode = True,就不需要手動把 ORM 物件轉成dict。
進階:多層嵌套的 ORM 物件
# models.py(加入關聯)
class Post(Base):
__tablename__ = "posts"
id = Column(Integer, primary_key=True, index=True)
title = Column(String, index=True)
content = Column(String)
owner_id = Column(Integer, ForeignKey("users.id"))
owner = relationship("User", back_populates="posts")
User.posts = relationship("Post", back_populates="owner")
# schemas.py(嵌套模型)
class PostSchema(BaseModel):
id: int
title: str
content: str
class Config:
orm_mode = True
class UserWithPostsSchema(BaseModel):
id: int
username: str
email: str
posts: list[PostSchema] = [] # 直接嵌套另一個 Pydantic 模型
class Config:
orm_mode = True
# main.py(使用嵌套回傳)
@app.get("/users/{user_id}/detail", response_model=schemas.UserWithPostsSchema)
def read_user_detail(user_id: int, db: Session = Depends(database.get_db)):
user = crud.get_user_with_posts(db, user_id) # 會 eager load posts
return user
小技巧:若使用
selectinload、joinedload等 eager loading 方法,確保關聯資料已被載入,避免在序列化時觸發額外的 lazy loading。
2. json_encoders:自訂 JSON 序列化行為
為什麼要用?
- Python 原生的 JSON 編碼器只能處理
str,int,float,bool,None與list/dict。 - 常見的
datetime,date,Decimal,UUID等型別會拋出TypeError。 json_encoders讓我們在模型層面定義「如何把這些型別轉成字串或其他 JSON 可接受的型別」。
基本範例:datetime 與 UUID
from pydantic import BaseModel
from datetime import datetime
from uuid import UUID
class ItemSchema(BaseModel):
id: UUID
name: str
created_at: datetime
class Config:
json_encoders = {
datetime: lambda v: v.isoformat(), # 轉成 ISO 8601 字串
UUID: lambda v: str(v), # 轉成字串
}
# 測試序列化
from uuid import uuid4
item = ItemSchema(
id=uuid4(),
name="測試商品",
created_at=datetime.utcnow()
)
print(item.json())
# 輸出: {"id":"c2a1b3e2-...", "name":"測試商品", "created_at":"2025-11-20T08:15:30.123456"}
進階:Decimal 與自訂類別
from decimal import Decimal
class PriceSchema(BaseModel):
amount: Decimal
currency: str = "TWD"
class Config:
json_encoders = {
Decimal: lambda v: f"{v:.2f}", # 轉成保留兩位小數的字串
}
price = PriceSchema(amount=Decimal("1234.5"))
print(price.json())
# {"amount":"1234.50","currency":"TWD"}
多個編碼器的合併
如果同一個模型需要同時處理 datetime、Decimal、UUID,只要把它們全部放進 json_encoders:
class ComplexSchema(BaseModel):
id: UUID
timestamp: datetime
price: Decimal
note: str | None = None
class Config:
json_encoders = {
UUID: str,
datetime: lambda v: v.isoformat(timespec="seconds"),
Decimal: lambda v: float(v), # 直接轉成 float
}
提醒:
json_encoders只在呼叫.json()、.dict()(by_alias=True)或 FastAPI 自動回傳時生效;若直接使用json.dumps(model),仍會走 Python 標準的序列化,需要自行傳入default參數或使用model.json()。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方式 |
|---|---|---|
忘記開 orm_mode |
回傳 ORM 物件會出現 value is not a valid dict 的錯誤。 |
一定在使用 ORM 物件的 Pydantic 模型內加入 class Config: orm_mode = True。 |
json_encoders 不會自動套用到子模型 |
若父模型定義了 json_encoders,子模型仍需要自行定義或繼承。 |
使用繼承:class ChildSchema(ParentSchema): ...,或在每個模型都設定 json_encoders。 |
datetime 時區問題 |
datetime.isoformat() 會把本地時間直接輸出,可能缺少時區資訊。 |
在 json_encoders 中使用 v.isoformat(timespec="seconds") + "Z" 或使用 pytz/zoneinfo 先轉成 UTC。 |
Decimal 直接轉成 float 可能失精度 |
金額資料若轉成 float,在極端情況下會出現四捨五入誤差。 |
建議轉成字串:lambda v: f"{v:.2f}",或使用 str(v)。 |
| 大量關聯資料導致性能瓶頸 | 開啟 orm_mode 後,若回傳的模型包含多層關聯,Pydantic 會遍歷所有屬性,可能觸發 N+1 查詢。 |
使用 eager loading(joinedload、selectinload),或在 response_model 中只返回必要欄位。 |
最佳實踐
統一管理 Config
建立一個基礎模型BaseSchema,所有其他模型繼承它,集中管理orm_mode與共用的json_encoders。class BaseSchema(BaseModel): class Config: orm_mode = True json_encoders = { datetime: lambda v: v.isoformat(timespec="seconds"), UUID: str, }使用
alias讓 API 更友好
若資料庫欄位使用 snake_case,前端偏好 camelCase,可在Config中設定allow_population_by_field_name = True並使用alias。class UserSchema(BaseSchema): id: int username: str = Field(..., alias="userName") email: str測試序列化
在單元測試中加入model.json()的斷言,確保json_encoders正確運作。def test_price_serialization(): price = PriceSchema(amount=Decimal("99.99")) assert price.json() == '{"amount":"99.99","currency":"TWD"}'
實際應用場景
場景一:從資料庫直接回傳使用者資訊
在一個社交平台,我們需要提供 GET /users/{id} 的 API,返回使用者的基本資訊以及最近的貼文。使用 orm_mode 可以讓我們直接把 SQLAlchemy 的 User 物件回傳,而不必額外寫 dict 轉換函式。
@app.get("/users/{id}", response_model=schemas.UserWithPostsSchema)
def get_user(id: int, db: Session = Depends(get_db)):
user = db.query(models.User).options(selectinload(models.User.posts)).get(id)
if not user:
raise HTTPException(status_code=404, detail="User not found")
return user # Pydantic 會自動使用 orm_mode 轉成 JSON
場景二:回傳金融交易資料,包含 Decimal 與 datetime
金融系統常需要精確的金額與交易時間。透過 json_encoders,我們可以保證金額以字串形式(避免浮點誤差)回傳,時間則以 ISO 8601 格式。
class TransactionSchema(BaseSchema):
id: UUID
amount: Decimal
timestamp: datetime
description: str | None = None
class Config(BaseSchema.Config):
json_encoders = {
**BaseSchema.Config.json_encoders,
Decimal: lambda v: f"{v:.4f}", # 四位小數
}
@app.get("/transactions/{tx_id}", response_model=TransactionSchema)
def get_transaction(tx_id: UUID, db: Session = Depends(get_db)):
tx = db.query(models.Transaction).get(tx_id)
return tx
場景三:API 需要支援多語系,將枚舉值自訂為字串
假設有一個訂單狀態的 Enum:
from enum import Enum
class OrderStatus(str, Enum):
pending = "pending"
shipped = "shipped"
delivered = "delivered"
若直接回傳,Pydantic 會輸出原始 Enum 名稱。若想要回傳更友好的文字(例如中文),可以利用 json_encoders:
class OrderSchema(BaseSchema):
id: int
status: OrderStatus
total: Decimal
class Config(BaseSchema.Config):
json_encoders = {
**BaseSchema.Config.json_encoders,
OrderStatus: lambda v: {"pending": "待處理", "shipped": "已出貨", "delivered": "已送達"}[v.value],
}
這樣前端就直接取得「待處理」等本地化文字,減少前端的映射工作。
總結
orm_mode讓 Pydantic 能無縫接受 ORM 物件,避免手動dict轉換,提高開發效率。json_encoders提供了自訂 JSON 序列化的彈性,解決datetime、Decimal、UUID、Enum 等常見類型的序列化問題。- 透過 基礎模型繼承、eager loading、測試序列化 等最佳實踐,我們可以在大型專案中保持模型的可讀性與效能。
把這兩個 Config 功能熟練掌握後,你的 FastAPI API 不僅能正確地對接資料庫模型,還能以乾淨、符合前端需求的 JSON 格式回傳資料,為整體系統的可維護性與擴充性奠定堅實基礎。祝你在 FastAPI 的開發旅程中玩得開心、寫得順手!