本文 AI 產出,尚未審核

FastAPI 教學:Pydantic 模型的 Config 設定(orm_modejson_encoders

簡介

在使用 FastAPI 開發 API 時,Pydantic 是負責資料驗證與序列化的核心工具。除了宣告欄位型別外,Pydantic 也提供了 Config 類別讓我們客製化模型的行為。其中最常用的兩個設定是 orm_modejson_encoders,它們分別解決了「如何直接使用 ORM 物件」以及「如何自訂 JSON 序列化」的問題。

  • 為什麼需要 orm_mode
    當我們的資料來源是 SQLAlchemy、Tortoise‑ORM 等 ORM 時,模型通常是 Python 物件而非字典。若不啟用 orm_mode,FastAPI 會在回傳時拋出錯誤,因為 Pydantic 預設只接受 dictBaseModel

  • 為什麼需要 json_encoders
    有些資料型別(例如 datetimeDecimalUUID)在預設的 JSON 序列化器中不被支援,會導致 TypeError: Object of type ... is not JSON serializable。透過 json_encoders,我們可以告訴 Pydantic 如何把這些型別轉成 JSON 可接受的格式。

本篇文章將從概念說明、實作範例、常見陷阱與最佳實踐,最後帶你了解實務應用的情境,幫助你在 FastAPI 專案中更得心應手地使用 Config


核心概念

1. orm_mode:讓 Pydantic 能直接讀取 ORM 物件

為什麼要用?

  • ORM 物件的屬性是以屬性存取(user.iduser.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

小技巧:若使用 selectinloadjoinedload 等 eager loading 方法,確保關聯資料已被載入,避免在序列化時觸發額外的 lazy loading。


2. json_encoders:自訂 JSON 序列化行為

為什麼要用?

  • Python 原生的 JSON 編碼器只能處理 str, int, float, bool, Nonelist/dict
  • 常見的 datetime, date, Decimal, UUID 等型別會拋出 TypeError
  • json_encoders 讓我們在模型層面定義「如何把這些型別轉成字串或其他 JSON 可接受的型別」。

基本範例:datetimeUUID

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"}

多個編碼器的合併

如果同一個模型需要同時處理 datetimeDecimalUUID,只要把它們全部放進 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 loadingjoinedloadselectinload),或在 response_model 中只返回必要欄位。

最佳實踐

  1. 統一管理 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,
            }
    
  2. 使用 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
    
  3. 測試序列化
    在單元測試中加入 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

場景二:回傳金融交易資料,包含 Decimaldatetime

金融系統常需要精確的金額與交易時間。透過 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 序列化的彈性,解決 datetimeDecimalUUID、Enum 等常見類型的序列化問題。
  • 透過 基礎模型繼承eager loading測試序列化 等最佳實踐,我們可以在大型專案中保持模型的可讀性與效能。

把這兩個 Config 功能熟練掌握後,你的 FastAPI API 不僅能正確地對接資料庫模型,還能以乾淨、符合前端需求的 JSON 格式回傳資料,為整體系統的可維護性與擴充性奠定堅實基礎。祝你在 FastAPI 的開發旅程中玩得開心、寫得順手!