本文 AI 產出,尚未審核

FastAPI 教學:Pydantic 模型中的 datetime 型別處理


簡介

在 Web API 開發中,時間資訊的傳遞與驗證 常是最容易出錯的環節。
FastAPI 以 Pydantic 為資料模型基礎,提供了強大的型別檢查與自動文件產生功能,而 datetimedatetimetimedelta 等時間相關型別則是日常開發不可或缺的部分。

掌握 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️⃣ 驗證 datetime 欄位的相容性

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_durationdateutil)或自行正則驗證

最佳實踐

  1. 全站統一 UTC:所有儲存與傳輸的 datetime 均使用 UTC,前端根據使用者時區自行轉換。
  2. 明確宣告時區:在 API 文件(OpenAPI)中標註 format: date-time,讓 Swagger UI 自動顯示時區說明。
  3. 使用 Pydantic 的 Field(..., example="2023-08-15T14:30:00Z") 提供範例,降低前端使用錯誤。
  4. 測試時覆蓋時區邊界(例如夏令時間切換),確保 validator 正確處理。
  5. 避免在模型中混用 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)是避免時間錯亂的根本策略,透過 validatorroot_validator 可輕鬆實現。
  • 客製化序列化json_encoders)與 自訂驗證@validator)讓我們能依需求控制時間字串的格式、精度與時區資訊。
  • 在實務開發中,務必在 模型層資料庫層前端層 三端保持一致的時間觀念,才能避免因時區或格式不一致而導致的錯誤。

掌握以上概念與技巧,你就能在 FastAPI 專案中自信地處理任何與時間相關的需求,從簡單的事件排程到複雜的跨時區金融交易,都能寫出 清晰、可靠且易於維護 的 API。祝開發順利! 🚀