FastAPI ‧ 資料驗證與轉換(Validation & Serialization)
主題:自訂 JSONEncoder
簡介
在 FastAPI 中,回傳的資料最終都會被序列化成 JSON,交給前端或其他服務使用。
FastAPI 內建的序列化機制主要仰賴 pydantic 的模型與 Python 標準的 json.dumps,但當我們的資料結構裡包含 datetime、Decimal、Enum、UUID、自訂類別或是 MongoDB ObjectId 等非 JSON 原生類型時,預設的編碼器會拋出 TypeError。
因此,自訂 JSONEncoder 成了必備的技巧:
- 讓 API 能正確回傳複雜物件。
- 統一格式(例如 datetime 要固定為 ISO 8601、Decimal 要保留小數點)。
- 簡化前端處理,減少資料轉型的成本。
本篇將從概念說明、實作範例、常見陷阱與最佳實踐,一直到真實專案中的應用情境,帶你完整掌握 FastAPI 的自訂 JSONEncoder。
核心概念
1. 為什麼需要自訂 JSONEncoder?
- 標準 JSON 只支援
str、int、float、bool、None、list、dict。 - Python 內建類型 如
datetime.datetime、decimal.Decimal、uuid.UUID、Enum等,都不在支援範圍。 - FastAPI 會先把 pydantic 模型轉成
dict,再交給jsonable_encoder→json.dumps。若json.dumps無法處理,就會出錯。
2. FastAPI 的兩條路徑
| 路徑 | 說明 | 何時使用 |
|---|---|---|
fastapi.encoders.jsonable_encoder + json.dumps |
手動呼叫,適合在 背景任務、WebSocket 或 測試 時使用。 | 想要自行控制序列化行為 |
自訂 JSONResponse(或全域 app.json_encoder) |
直接掛在 FastAPI 應用層,所有回傳的 Response 都會走自訂編碼器。 |
想要一次解決所有端點的序列化需求 |
小技巧:如果只需要在少數端點調整,可在
Response建構子裡傳入json_dumps=custom_json_dumps;若全局需求,直接設定app.json_encoder。
3. 建立自訂 JSONEncoder 的步驟
- 繼承自
json.JSONEncoder。 - 覆寫
default(self, obj):檢查obj的類型,回傳可 JSON 序列化的值。 - 在 FastAPI 中註冊:
from fastapi import FastAPI app = FastAPI() app.json_encoder = MyJSONEncoder # 全局設定
4. jsonable_encoder 與自訂編碼器的關係
jsonable_encoder 會先把 pydantic、ORM、datetime 等物件轉成「JSON 可接受」的 Python 基礎型別(例如 datetime → str、Enum → value)。
若你在 default 中再次處理相同類型,可能會重複轉換。
最佳實踐是:讓 jsonable_encoder 處理大部分常見類型,只在 default 裡處理「特殊」或「自訂」類別。
程式碼範例
以下範例皆以 Python 3.11、FastAPI 0.110 為基礎,使用 pydantic 版型。
範例 1:最簡單的自訂 JSONEncoder(支援 datetime 與 UUID)
# file: encoder.py
import json
from datetime import datetime, date
from uuid import UUID
class MyJSONEncoder(json.JSONEncoder):
"""將 datetime、date、UUID 轉成字串"""
def default(self, obj):
if isinstance(obj, (datetime, date)):
# ISO 8601 格式,保持時區資訊
return obj.isoformat()
if isinstance(obj, UUID):
return str(obj)
# 交給父類別處理其他類型
return super().default(obj)
# main.py
from fastapi import FastAPI
from fastapi.responses import JSONResponse
from encoder import MyJSONEncoder
from uuid import uuid4
from datetime import datetime
app = FastAPI()
app.json_encoder = MyJSONEncoder # 全域註冊
@app.get("/info")
def get_info():
return {
"now": datetime.utcnow(),
"id": uuid4()
}
說明:
app.json_encoder = MyJSONEncoder後,所有回傳的dict會自動走MyJSONEncoder。- 前端收到的
now為"2025-11-20T07:12:34.567890Z",id為"c1b2a3e4-5d6f-7a8b-9c0d-1e2f3a4b5c6d"。
範例 2:支援 Decimal 與 Enum,且保留小數點精度
# file: models.py
from decimal import Decimal
from enum import Enum
from pydantic import BaseModel
class Currency(str, Enum):
USD = "USD"
TWD = "TWD"
EUR = "EUR"
class Product(BaseModel):
name: str
price: Decimal # 需要保留小數點
currency: Currency
# file: encoder.py(續)
from decimal import Decimal
from enum import Enum
class MyJSONEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, Decimal):
# 轉成字串,避免 float 精度遺失
return str(obj)
if isinstance(obj, Enum):
# Enum 直接回傳 value
return obj.value
return super().default(obj)
# main.py(續)
from models import Product, Currency
from decimal import Decimal
@app.get("/product")
def get_product():
p = Product(
name="FastAPI 教學書",
price=Decimal("199.99"),
currency=Currency.TWD
)
return p
說明:
Decimal會被序列化成字串"199.99",前端若要做數值運算可自行轉型。Enum只回傳value,讓 JSON 更易讀。
範例 3:自訂類別(例如 MongoDB 的 ObjectId)
# file: encoder.py(續)
from bson import ObjectId # pip install pymongo
class MyJSONEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, ObjectId):
# ObjectId 直接轉成 24 位十六進位字串
return str(obj)
return super().default(obj)
# main.py(續)
from bson import ObjectId
@app.get("/user/{uid}")
def get_user(uid: str):
# 假設從 MongoDB 取回的文件
user_doc = {
"_id": ObjectId(uid),
"name": "Alice",
"created_at": datetime.utcnow()
}
return user_doc
說明:
ObjectId不是標準 JSON 類型,透過自訂編碼器直接轉成字串,前端只要把它當作唯一識別碼即可。
範例 4:在單一端點使用 JSONResponse 搭配自訂 json_dumps
from fastapi.responses import JSONResponse
import json
def custom_json_dumps(v, *, default):
# 直接使用我們的編碼器,省去手動設定 app.json_encoder
return json.dumps(v, cls=MyJSONEncoder, ensure_ascii=False)
@app.get("/custom")
def custom_endpoint():
data = {
"now": datetime.now(),
"price": Decimal("1234.56")
}
return JSONResponse(content=data, json_dumps=custom_json_dumps)
說明:
- 只想在這個端點使用自訂序列化時,直接把
json_dumps傳給JSONResponse即可,不會影響其他路由。
範例 5:結合 jsonable_encoder 與自訂 Encoder(最佳實踐)
from fastapi.encoders import jsonable_encoder
@app.post("/order")
def create_order(order: dict):
# 1. 先使用 jsonable_encoder 處理 pydantic、datetime 等常見類型
safe_data = jsonable_encoder(order, custom_encoder={Decimal: str})
# 2. 再交給自訂 JSONEncoder 處理特殊類型(例如 ObjectId)
return JSONResponse(content=safe_data, json_dumps=lambda v, **_: json.dumps(v, cls=MyJSONEncoder))
說明:
jsonable_encoder內建支援datetime、Enum、UUID等,並允許傳入custom_encoder針對Decimal做字串化。- 之後再交給
MyJSONEncoder處理剩餘的特殊類型,形成 兩層防護,避免遺漏。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解法 / Best Practice |
|---|---|---|
忘記 ensure_ascii=False |
中文會被轉成 Unicode \uXXXX,不友好。 |
在 json.dumps 時加上 ensure_ascii=False,或在 JSONResponse 內部設定。 |
| 重複序列化 | 同時使用 jsonable_encoder 與 default 處理 datetime,會得到兩次 isoformat,導致字串變成 "2025-11-20T07:12:34Z" → "\"2025-...\""。 |
只在一個地方處理,建議先讓 jsonable_encoder 處理常見類型,default 只處理「自訂」類別。 |
忘記回傳 super().default(obj) |
若 default 沒有呼叫父類別,非預期類型會拋出 TypeError,導致 API 500。 |
永遠在最後 return super().default(obj),讓 Python 處理未知類型。 |
使用 list 或 dict 的子類別 |
自訂類別繼承自 list/dict 時,json.dumps 可能直接把它當成原生型別,跳過 default。 |
若需要自訂行為,覆寫 __iter__ 或在 default 中檢查子類別的實例。 |
| 全域設定影響測試 | app.json_encoder 會影響測試環境,導致測試資料不易比對。 |
在測試套件的 fixture 中臨時恢復預設 json_encoder,或在測試時使用 client.get(..., json_dumps=...)。 |
最佳實踐總結
- 先使用
jsonable_encoder處理大部分資料型別。 - 自訂
JSONEncoder只針對「真的無法被jsonable_encoder處理」的類別。 - 全域註冊 (
app.json_encoder) 方便統一管理;若只在少數端點需要,使用JSONResponse(json_dumps=…)。 - 保持
ensure_ascii=False,讓中文直接輸出。 - 寫單元測試,檢查特殊類別的序列化結果,避免因升級 FastAPI 而產生回傳格式變動。
實際應用場景
| 場景 | 為什麼需要自訂 JSONEncoder | 解決方式 |
|---|---|---|
金融系統:回傳金額使用 Decimal,避免浮點誤差。 |
Decimal 會被 json.dumps 拒絕。 |
在 JSONEncoder.default 中將 Decimal 轉成字串,或在 jsonable_encoder 設 custom_encoder={Decimal: str}。 |
IoT 平台:感測器資料帶有 datetime、Enum、自訂 SensorStatus 類別。 |
前端需要 ISO8601 時間與 Enum 的文字說明。 | 用 MyJSONEncoder 處理 datetime、Enum,同時在模型中加入 json_encoders 讓 pydantic 直接支援。 |
電商後端:商品 ID 使用 MongoDB ObjectId,同時回傳 UUID 作為追蹤碼。 |
ObjectId、UUID 都不是 JSON 標準類型。 |
在全域 MyJSONEncoder 中同時處理 ObjectId 與 UUID,確保所有端點都能正確回傳。 |
| 多語系 API:回傳的文字資料含有繁體中文、日文、emoji。 | 若未設定 ensure_ascii=False,中文會被轉成 Unicode 編碼。 |
在 JSONResponse 或 json.dumps 設定 ensure_ascii=False,讓文字直接呈現。 |
大型微服務:不同服務使用不同的序列化規則(例如某服務要把 datetime 只保留日期)。 |
同一套 FastAPI 應用無法同時滿足所有需求。 | 為特定路由建立 自訂 Response(如 DateOnlyJSONResponse)或在路由內部使用 jsonable_encoder(..., custom_encoder={datetime: lambda d: d.date().isoformat()})。 |
總結
- FastAPI 的回傳資料最終會走 JSON 序列化,標準編碼器只能處理基本型別。
- 透過 自訂
JSONEncoder,我們可以把datetime、UUID、Decimal、Enum、ObjectId等特殊類別安全地轉成字串或其他 JSON 可接受的型別。 - 最佳實踐:先利用
jsonable_encoder處理常見類型,再在default中僅針對「自訂」或「第三方」類別做轉換;全域設定app.json_encoder讓整個應用保持一致,必要時可在單一端點使用JSONResponse(json_dumps=…)。 - 注意
ensure_ascii=False、避免重複序列化、以及在測試環境中適當還原編碼器設定,都是提升開發效率與 API 穩定性的關鍵。
掌握了自訂 JSONEncoder 後,你的 FastAPI 專案將能更從容地面對各種複雜資料結構,提供乾淨、可預測的 API 介面,讓前端與其他服務的整合變得更順暢。祝你開發愉快 🎉