本文 AI 產出,尚未審核

FastAPI – Pydantic 模型(Request / Response Models)

欄位型別註記與預設值


簡介

FastAPI 中,所有的請求與回應資料都是透過 Pydantic 模型來定義與驗證的。
欄位的 型別註記(type annotation)預設值(default value) 不只是語法糖,它們直接決定了:

  1. 自動產生的 OpenAPI 文件(讓前端與第三方系統一目了然)。
  2. 輸入資料的驗證邏輯(錯誤回傳、型別轉換、欄位必填與否)。
  3. 回應資料的序列化(確保回傳 JSON 的結構與欄位型別正確)。

對於剛接觸 FastAPI 的開發者來說,正確使用型別註記與預設值是避免 API 失敗與維護成本飆升的關鍵。本篇文章將從概念說明、實作範例、常見陷阱與最佳實踐,一步步帶你掌握 Pydantic 欄位型別與預設值 的使用技巧,並提供實務應用情境,讓你能在專案中即刻上手。


核心概念

1. 型別註記的基本原則

Pydantic 依賴 Python 的 型別提示(type hints)來決定每個欄位的資料型別。常見的型別包括:

型別 說明 範例
int 整數 age: int
float 浮點數 price: float
str 字串 name: str
bool 布林值 is_active: bool
datetime 日期時間 created_at: datetime
list[T] / Tuple[T, ...] 陣列或元組 tags: List[str]
dict[str, T] 字典 metadata: Dict[str, Any]
Enum 列舉型別 status: StatusEnum
Optional[T] 可為 None(即非必填) nickname: Optional[str] = None

重要:Pydantic 會在模型實例化時自動 轉換(coerce)相容的資料,例如把字串 "123" 轉成 int 123,或把 "2023-01-01" 轉成 datetime。若轉換失敗,會拋出 ValidationError,FastAPI 會自動回傳 422 錯誤給前端。

2. 預設值的作用

預設值有兩大功能:

  1. 標示欄位非必填:若欄位有預設值,即使請求中未提供該欄位,模型仍會成功建立,並使用預設值。
  2. 提供業務邏輯的預設行為:例如 page: int = 1page_size: int = 20,讓分頁 API 在未指定時仍能正常運作。

2.1 必填 vs. 非必填

定義方式 必填? 說明
field: int ✅ 必填 未提供會產生 422
field: int = 0 ❌ 非必填 未提供會使用 0
field: Optional[int] = None ❌ 非必填 未提供會是 None

小技巧:若僅想讓欄位允許 None,但仍想保留型別檢查,使用 Optional[T],而不是直接 T = None(後者會讓型別變成 Any)。

3. 進階型別與驗證

3.1 conint, confloat, constr

Pydantic 提供 限制型別(constrained types)讓你在宣告時就加入驗證規則:

from pydantic import BaseModel, conint, condecimal, constr

class ProductCreate(BaseModel):
    # 價格必須大於 0,且最多兩位小數
    price: condecimal(gt=0, max_digits=10, decimal_places=2)
    # 庫存只能是非負整數
    stock: conint(ge=0)
    # 商品名稱長度 1~100
    name: constr(min_length=1, max_length=100)

3.2 Field 與額外資訊

Field 允許你設定 描述、別名、範例值,同時支援 JSON Schema 的額外屬性,對於自動產生的 API 文件非常有幫助:

from pydantic import BaseModel, Field
from typing import List

class OrderQuery(BaseModel):
    # 別名可讓前端使用 snake_case 或 camelCase
    page: int = Field(1, ge=1, description="頁碼,從 1 開始")
    page_size: int = Field(20, ge=1, le=100, description="每頁顯示筆數")
    # 使用別名
    sort_by: str = Field("created_at", alias="sortBy", description="排序欄位")
    # 範例值會出現在 Swagger UI
    tags: List[str] = Field(default_factory=list, example=["electronics", "sale"])

4. 範例:從 Request 到 Response 的完整流程

以下示範一個「建立使用者」的 API,涵蓋 請求模型回應模型預設值型別註記

# file: main.py
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, EmailStr, Field
from typing import Optional
from datetime import datetime

app = FastAPI(title="User API 範例")

class UserCreateRequest(BaseModel):
    """建立使用者的請求模型"""
    username: str = Field(..., min_length=3, max_length=20, description="使用者名稱")
    email: EmailStr = Field(..., description="有效的 Email 地址")
    age: Optional[int] = Field(None, ge=0, le=150, description="年齡,若未提供則為 None")
    is_active: bool = Field(True, description="是否啟用帳號,預設為 True")

class UserResponse(BaseModel):
    """回傳給前端的使用者資料模型"""
    id: int = Field(..., description="系統自動產生的使用者 ID")
    username: str
    email: EmailStr
    age: Optional[int] = None
    is_active: bool
    created_at: datetime = Field(default_factory=datetime.utcnow)

# 模擬資料庫
fake_db = {}
next_id = 1

@app.post("/users", response_model=UserResponse, status_code=201)
def create_user(payload: UserCreateRequest):
    global next_id
    # 簡易檢查:Email 必須唯一
    if any(u["email"] == payload.email for u in fake_db.values()):
        raise HTTPException(status_code=400, detail="Email already exists")
    user = payload.dict()
    user["id"] = next_id
    user["created_at"] = datetime.utcnow()
    fake_db[next_id] = user
    next_id += 1
    return user

說明

  • UserCreateRequest 中的 is_active 設定了預設值 True,因此即使前端不傳此欄位,資料仍會被視為啟用。
  • age 使用 Optional[int] = None,表示此欄位可省略或傳 null
  • UserResponse 透過 default_factory=datetime.utcnowcreated_at 自動產生時間戳。
  • response_model=UserResponse 讓 FastAPI 在回傳前自動過濾、序列化,並產生正確的 OpenAPI 文件。

常見陷阱與最佳實踐

陷阱 為什麼會發生 解決方式
預設值與 Optional 搭配錯誤 直接寫 field: Optional[int] = 0 會把型別變成 int(因為預設值非 None),導致驗證不符合預期。 若想允許 None,必須將預設值設為 None,或使用 `field: int
使用可變物件作為預設值 field: List[int] = [] 會在每次實例化時共用同一個列表,導致資料互相污染。 使用 default_factory=listField(default_factory=list)
別名(alias)與 populate_by_name 忽略 當前端傳送 camelCase 時,模型若未開啟 populate_by_name,會拋出 422。 BaseModel.Config 中設定 allow_population_by_field_name = True,或在 FastAPI 路由上使用 response_model_by_alias=False
過度依賴 Any 把欄位型別寫成 Any 會失去驗證與文件生成的好處。 盡量使用具體型別或 Union,必要時才使用 Any
忽略 example / description Swagger UI 沒有說明,前端開發者不易理解。 Field 中加入 descriptionexample,提升可讀性。

最佳實踐清單

  1. 始終使用型別註記:即使是 str 也要明確寫 username: str,讓 Pydantic 能正確產生 JSON Schema。
  2. 預設值即代表非必填:若欄位必填,使用 ...(Ellipsis)或不提供預設值。
  3. 避免可變物件作為預設值:使用 default_factory
  4. 善用 Field:加入 descriptionexamplege/le 等限制,讓文件自動說明。
  5. 測試驗證邏輯:使用 client.post(..., json=payload) 測試各種缺失與錯誤情況,確保 422 錯誤訊息清晰。

實際應用場景

1. 分頁 API

class PaginationParams(BaseModel):
    page: int = Field(1, ge=1, description="第幾頁")
    size: int = Field(20, ge=1, le=100, description="每頁筆數")
  • 好處:前端不傳參數時會自動使用預設值,避免必填錯誤。

2. 多語系文字欄位

class ProductCreate(BaseModel):
    name: dict = Field(..., description="多語系商品名稱,例如 {'en': 'Apple', 'zh': '蘋果'}")
    price: float = Field(..., gt=0)
  • 說明:使用 dict 作為欄位型別,並在 description 中說明結構,讓前端明白 JSON 結構。

3. 啟用/停用功能的切換

class FeatureToggle(BaseModel):
    feature_name: str
    enabled: bool = Field(True, description="預設為啟用")
  • 情境:管理平台可一次更新多個功能的狀態,若未傳 enabled,系統會保留原本的狀態或使用預設值。

4. 日期範圍查詢

from datetime import date

class DateRangeQuery(BaseModel):
    start_date: date = Field(..., description="查詢起始日期")
    end_date: date = Field(..., description="查詢結束日期")
    include_weekends: bool = Field(False, description="是否包含週末,預設不包含")
  • 優點date 型別會自動從字串轉換,若格式錯誤會直接返回 422,減少手動驗證程式碼。

總結

  • 型別註記是 Pydantic 驗證與 FastAPI 自動產生 OpenAPI 文件的根基,務必正確、完整。
  • 預設值不只是「沒有傳就用什麼」的設定,它同時決定欄位是否為必填,並能提供業務邏輯的預設行為。
  • 使用 Fielddefault_factory、限制型別(如 conintconstr)可以在模型層級完成大部分驗證,讓路由函式保持簡潔。
  • 注意 可變預設值、別名、Optional 的細節,避免常見的驗證失誤與資料污染。
  • 在實務開發中,將 請求模型回應模型分頁/篩選參數 都抽成獨立的 Pydantic 類別,不僅提升程式可讀性,也讓 API 文件自動保持同步。

掌握了欄位型別註記與預設值的正確寫法,你的 FastAPI 專案將能在 驗證安全、文件完整、開發效率 三方面同時受益。祝你開發順利,打造出高品質的 RESTful API!