FastAPI 教學:Pydantic 模型中的 datetime 型別處理
簡介
在 Web API 開發中,時間資訊的傳遞與驗證 常是最容易出錯的環節。
FastAPI 以 Pydantic 為資料模型基礎,提供了強大的型別檢查與自動文件產生功能,而 datetime、date、time、timedelta 等時間相關型別則是日常開發不可或缺的部分。
掌握 datetime 型別的序列化 / 反序列化、時區處理與驗證規則,能讓 API:
- 正確地接受前端送來的時間字串
- 在回傳時以 ISO 8601 標準呈現,確保跨平台相容性
- 減少因時區混亂而產生的錯誤(例如:資料庫儲存與顯示不一致)
本篇文章將從概念說明、實作範例、常見陷阱到最佳實踐,完整介紹在 FastAPI 中使用 Pydantic 處理 datetime 的方法,適合 初學者到中階開發者 快速上手。
核心概念
1. Pydantic 對 datetime 的預設行為
- 自動解析:Pydantic 會自動把符合 ISO 8601 格式的字串轉成
datetime.datetime物件。 - 序列化:回傳時會把
datetime物件轉成 ISO 8601 字串(包含時區資訊),除非自行覆寫json_encoders。
Tip:若前端傳遞的時間字串不是 ISO 格式,需要自行定義驗證器(validator)或使用
custom_encoders。
2. 時區(timezone)處理
datetime 物件分為 naive(無時區)與 aware(有時區)兩種。
在 API 中,建議統一使用 UTC aware datetime,再於前端根據使用者時區轉換。
Pydantic 內建的 datetime 解析會保留字串中的時區資訊;若字串未帶時區,會產生 naive 物件。此時可利用 validator 強制轉為 UTC。
3. 常用欄位型別
| 型別 | 說明 | 範例字串 |
|---|---|---|
datetime |
包含日期與時間,支援時區 | 2023-08-15T14:30:00+08:00 |
date |
只包含日期 | 2023-08-15 |
time |
只包含時間,支援時區 | 14:30:00+08:00 |
timedelta |
時間間隔 | P1DT2H3M4S(ISO 8601 duration) |
程式碼範例
以下示範 5 個實用範例,涵蓋基本模型、時區強制、客製化序列化、驗證器與自訂 JSON Encoder。
1️⃣ 基本 Request / Response Model
from fastapi import FastAPI
from pydantic import BaseModel
from datetime import datetime
app = FastAPI()
class EventCreate(BaseModel):
name: str
start_time: datetime # 直接使用 datetime
class EventResponse(BaseModel):
id: int
name: str
start_time: datetime
@app.post("/events/", response_model=EventResponse)
async def create_event(event: EventCreate):
# 假設寫入資料庫後取得 id
return EventResponse(id=1, **event.dict())
說明:只要前端送來符合 ISO8601 的字串(例如
2023-08-15T14:30:00+08:00),Pydantic 會自動轉成datetime。
2️⃣ 強制轉為 UTC aware datetime
from pydantic import validator
import pytz
class EventCreateUTC(BaseModel):
name: str
start_time: datetime
@validator("start_time", pre=True)
def ensure_utc(cls, v):
# 若是字串,先交給 Pydantic 解析
dt = v if isinstance(v, datetime) else datetime.fromisoformat(v)
if dt.tzinfo is None:
# 假設預設為台北時間 (+08:00)
dt = dt.replace(tzinfo=pytz.timezone("Asia/Taipei"))
# 轉成 UTC
return dt.astimezone(pytz.UTC)
重點:
pre=True讓驗證器在 Pydantic 解析前先處理原始資料,避免產生 naive datetime。
3️⃣ 客製化序列化:只保留毫秒、去除時區
from pydantic import BaseModel
from datetime import datetime
class EventResponseCustom(BaseModel):
id: int
name: str
start_time: datetime
class Config:
json_encoders = {
datetime: lambda v: v.replace(tzinfo=None).isoformat(timespec="milliseconds")
}
# 回傳結果會是 "2023-08-15T14:30:00.123"
使用情境:某些前端只能接受毫秒級的時間字串,或是後端決定不回傳時區資訊。
4️⃣ 驗證 date 與 time 欄位的相容性
from pydantic import BaseModel, validator
from datetime import date, time
class Schedule(BaseModel):
event_date: date
start_time: time # 只接受時間,不含日期
@validator("start_time")
def check_seconds(cls, v):
# 若秒數不為 0,拋出錯誤
if v.second != 0:
raise ValueError("seconds must be 0")
return v
說明:透過 validator 可自行加入業務規則,例如「會議只能在整分鐘開始」。
5️⃣ 使用 timedelta 計算結束時間
from pydantic import BaseModel, root_validator
from datetime import datetime, timedelta
class Meeting(BaseModel):
start: datetime
duration: timedelta # 例如 "PT1H30M"
@root_validator
def compute_end(cls, values):
start, duration = values.get("start"), values.get("duration")
values["end"] = start + duration
return values
end: datetime | None = None # 由 validator 填入
範例請求:
{ "start": "2023-08-15T09:00:00+08:00", "duration": "PT1H30M" }回傳:
end欄位會自動計算為2023-08-15T10:30:00+08:00。
常見陷阱與最佳實踐
| 陷阱 | 可能的結果 | 解決方式 |
|---|---|---|
| 傳入 naive datetime(未帶時區) | 時區混亂、資料庫儲存錯誤 | 使用 validator 強制加上時區或轉成 UTC |
前端使用非 ISO8601 格式(如 2023/08/15 14:30) |
解析失敗拋出 422 錯誤 | 在前端統一使用 toISOString(),或自訂 Pydantic validator 處理多種格式 |
| JSON 序列化時失去時區資訊 | 前端顯示錯誤時間 | 在 Config.json_encoders 中保留 tzinfo,或回傳 UTC 並在前端轉換 |
使用 datetime.now() 產生 naive 時間寫入 DB |
DB 內部時間與 API 不一致 | 使用 datetime.utcnow().replace(tzinfo=timezone.utc) 或 pytz.UTC |
timedelta 字串未符合 ISO 8601 |
解析失敗 | 可使用 parse_duration(dateutil)或自行正則驗證 |
最佳實踐:
- 全站統一 UTC:所有儲存與傳輸的
datetime均使用 UTC,前端根據使用者時區自行轉換。 - 明確宣告時區:在 API 文件(OpenAPI)中標註
format: date-time,讓 Swagger UI 自動顯示時區說明。 - 使用 Pydantic 的
Field(..., example="2023-08-15T14:30:00Z")提供範例,降低前端使用錯誤。 - 測試時覆蓋時區邊界(例如夏令時間切換),確保
validator正確處理。 - 避免在模型中混用 naive 與 aware,若必須混用,明確在程式碼註解說明原因。
實際應用場景
| 場景 | 為何需要 datetime 處理 | 典型實作 |
|---|---|---|
| 行程預約系統 | 使用者會選擇本地時區的時間,後端須轉成 UTC 儲存 | 在 CreateAppointment 模型中使用 validator 轉換為 UTC |
| 金融交易平台 | 交易時間必須精確到毫秒,且不可有時區歧義 | 使用 datetime + json_encoders 只保留毫秒,並在 DB 中以 TIMESTAMP WITH TIME ZONE 儲存 |
| IoT 裝置上報 | 裝置可能只傳送 epoch 秒,需要轉成 datetime |
在 SensorData 模型中使用 @validator(pre=True) 把 int 轉成 UTC datetime |
| 報表產生 | 需要根據使用者時區產生「今天」或「本月」報表 | 先把 date 轉成使用者時區的 datetime,再計算起始與結束時間 |
| 多國語系平台 | 不同地區的使用者在同一時間點看到相同的活動資訊 | 統一使用 UTC,前端根據 Intl.DateTimeFormat 轉換成當地時間顯示 |
總結
- Pydantic 為 FastAPI 提供了即時的
datetime解析與驗證,讓 API 能自動接受 ISO 8601 時間字串。 - 時區統一(建議使用 UTC)是避免時間錯亂的根本策略,透過
validator或root_validator可輕鬆實現。 - 客製化序列化(
json_encoders)與 自訂驗證(@validator)讓我們能依需求控制時間字串的格式、精度與時區資訊。 - 在實務開發中,務必在 模型層、資料庫層、前端層 三端保持一致的時間觀念,才能避免因時區或格式不一致而導致的錯誤。
掌握以上概念與技巧,你就能在 FastAPI 專案中自信地處理任何與時間相關的需求,從簡單的事件排程到複雜的跨時區金融交易,都能寫出 清晰、可靠且易於維護 的 API。祝開發順利! 🚀