本文 AI 產出,尚未審核

FastAPI 教學:Pydantic 模型的欄位別名(alias)

簡介

在使用 FastAPI 建立 API 時,我們往往會以 Pydantic 模型作為 Request / Response 的資料結構。實務上,前端或第三方系統送來的 JSON 鍵名不一定能直接對應到我們在程式碼裡定義的屬性名稱,這時 欄位別名(alias) 就派上用場。

透過別名,我們可以:

  • 保持程式碼的可讀性:使用符合 Python 命名慣例的屬性名稱(如 user_id
  • 兼容外部規範:仍能接受或回傳符合外部 API 規範的鍵名(如 userIduser-id
  • 避免破壞既有客戶端:在不改變前端合約的前提下,調整後端模型結構

本篇將深入說明 Pydantic 中的別名機制,並以 FastAPI 為例展示實作方式、常見陷阱與最佳實踐,幫助你在真實專案中輕鬆應對多樣化的資料格式。


核心概念

1. 為什麼需要別名?

在 JSON 中,鍵名的命名規則相當彈性,常見的形式有:

風格 範例
snake_case user_id
camelCase userId
kebab-case user-id
PascalCase UserId

而 Python 社群慣用 snake_case 作為變數與屬性名稱。如果直接以 userId 為屬性名,程式碼會顯得不自然;相反地,如果僅保留 user_id,卻又必須接受 userId 的請求,就會產生不匹配的問題。alias 正是為了解決這兩者之間的差異。

2. Pydantic 中的 aliasallow_population_by_field_name

在 Pydantic(v1)中,我們可以在模型欄位上使用 Field(..., alias="外部鍵名") 來指定別名。若想在建立模型時同時接受「欄位名稱」與「別名」兩種寫法,需要在模型 Config 中開啟 allow_population_by_field_name = True

from pydantic import BaseModel, Field

class UserIn(BaseModel):
    user_id: int = Field(..., alias="userId")
    email: str

    class Config:
        # 允許使用欄位名稱(user_id)或別名(userId)來建立實例
        allow_population_by_field_name = True

重點:若未設定 allow_population_by_field_name,僅能使用別名(userId)建立模型,欄位名稱(user_id)會被視為無效鍵。

3. 別名的方向:輸入 vs 輸出

  • 輸入(Request):別名用於 解析 前端傳來的 JSON。
  • 輸出(Response):別名決定 序列化 時的鍵名。

Pydantic 允許分別設定 alias(解析)與 alias_generator(序列化)。在 FastAPI 中,回傳模型會自動使用別名,除非明確設定 by_alias=False

from fastapi import FastAPI
from pydantic import BaseModel, Field

app = FastAPI()

class ItemOut(BaseModel):
    item_id: int = Field(..., alias="itemId")
    name: str

    class Config:
        # 回傳時使用別名
        orm_mode = True
        allow_population_by_field_name = True

@app.get("/items/{item_id}", response_model=ItemOut)
def read_item(item_id: int):
    # 假設從資料庫取出的欄位名稱是 snake_case
    return {"item_id": item_id, "name": "範例商品"}

回傳的 JSON 會是:

{
  "itemId": 1,
  "name": "範例商品"
}

4. 動態產生別名:alias_generator

如果整個專案的鍵名風格統一(例如全部使用 camelCase),手動為每個欄位寫 alias="..." 會很繁瑣。此時可以在 Config 中提供一個函式,讓 Pydantic 自動為每個欄位產生別名。

def to_camel(string: str) -> str:
    parts = string.split('_')
    return parts[0] + ''.join(word.capitalize() for word in parts[1:])

class Product(BaseModel):
    product_id: int
    product_name: str
    price: float

    class Config:
        alias_generator = to_camel
        allow_population_by_field_name = True
  • 輸入 JSON 可以是 productIdproductNameprice
  • 輸出時會自動使用 camelCase 鍵名。

5. 別名在 Nested Model 中的行為

別名同樣會在巢狀模型(Nested Model)中傳遞。只要每個子模型都有正確的 Config,FastAPI 會遞迴套用別名。

class Address(BaseModel):
    street_name: str = Field(..., alias="streetName")
    city: str

    class Config:
        allow_population_by_field_name = True

class User(BaseModel):
    user_id: int = Field(..., alias="userId")
    address: Address

    class Config:
        allow_population_by_field_name = True

程式碼範例

以下提供 5 個實用範例,從最基礎到較進階的情境,說明別名的各種使用方式。

範例 1:最簡單的別名

from fastapi import FastAPI
from pydantic import BaseModel, Field

app = FastAPI()

class LoginRequest(BaseModel):
    username: str
    password: str = Field(..., alias="pwd")   # 前端傳遞的鍵名是 pwd

    class Config:
        allow_population_by_field_name = True

@app.post("/login")
def login(req: LoginRequest):
    # 此時 req.password 仍可直接使用
    return {"msg": f"歡迎 {req.username}"}

說明:前端會送 {"username":"alice","pwd":"1234"},後端仍以 password 變數存取。

範例 2:同時支援別名與欄位名稱

class UpdateUser(BaseModel):
    user_id: int = Field(..., alias="userId")
    email: str

    class Config:
        allow_population_by_field_name = True

# 呼叫方式皆可
UpdateUser(user_id=1, email="a@b.com")          # 使用欄位名稱
UpdateUser(userId=1, email="a@b.com")          # 使用別名

技巧:在測試或 CLI 工具(如 httpie)時,可選擇較直觀的寫法。

範例 3:使用 alias_generator 產生 camelCase 別名

def to_camel(s: str) -> str:
    parts = s.split('_')
    return parts[0] + ''.join(p.title() for p in parts[1:])

class Order(BaseModel):
    order_id: int
    order_date: str
    total_amount: float

    class Config:
        alias_generator = to_camel
        allow_population_by_field_name = True

# 前端傳入的 JSON
payload = {"orderId": 1001, "orderDate": "2024-10-01", "totalAmount": 199.9}
order = Order(**payload)   # 正常解析

應用:當整個專案的 API 標準是 camelCase 時,僅寫一次 alias_generator 即可。

範例 4:回傳時使用別名(by_alias=True

class ProductOut(BaseModel):
    product_id: int = Field(..., alias="productId")
    name: str
    price: float

    class Config:
        orm_mode = True

@app.get("/products/{pid}", response_model=ProductOut, response_model_by_alias=True)
def get_product(pid: int):
    # 假設資料庫回傳的欄位是 snake_case
    return {"product_id": pid, "name": "筆記型電腦", "price": 24999}

回傳結果:

{
  "productId": 1,
  "name": "筆記型電腦",
  "price": 24999
}

注意response_model_by_alias=True 為 FastAPI 內建參數,若不寫則預設使用欄位名稱。

範例 5:巢狀模型的別名與驗證

class Profile(BaseModel):
    first_name: str = Field(..., alias="firstName")
    last_name: str = Field(..., alias="lastName")

    class Config:
        allow_population_by_field_name = True

class RegisterRequest(BaseModel):
    email: str
    password: str
    profile: Profile

    class Config:
        allow_population_by_field_name = True

@app.post("/register")
def register(req: RegisterRequest):
    # 直接使用 snake_case 欄位
    full_name = f"{req.profile.first_name} {req.profile.last_name}"
    return {"msg": f"{req.email} 註冊成功,歡迎 {full_name}"}

前端傳入的 JSON:

{
  "email": "bob@example.com",
  "password": "secret",
  "profile": {
    "firstName": "Bob",
    "lastName": "Builder"
  }
}

重點:即使是巢狀結構,只要每層模型都設定 allow_population_by_field_name=True,即可無痛解析。


常見陷阱與最佳實踐

陷阱 說明 解決方案
忘記開啟 allow_population_by_field_name 只寫了 alias,卻在程式內使用欄位名稱建立模型,會拋出 ValidationError 在每個使用別名的模型 Config 中加上 allow_population_by_field_name = True
別名與欄位名稱衝突 若別名恰好與其他欄位名稱相同,會導致解析時的模糊性。 避免使用相同字串作為別名,或使用 alias_priority(Pydantic v2)指定優先順序。
回傳時忘記使用 by_alias=True 預設回傳會使用欄位名稱,前端可能收到不符合合約的鍵名。 在 FastAPI 的路由設定 response_model_by_alias=True,或在 jsonable_encoder(..., by_alias=True) 中指定。
別名過長或不易讀 為了符合外部規範,別名可能變得冗長,降低程式碼可讀性。 使用 alias_generator 統一轉換規則,保持程式碼簡潔。
Pydantic v2 變更 v2 引入 model_configserialization_alias,舊寫法可能失效。 讀官方遷移文件,將 Config 改寫為 model_config = ConfigDict(...),或使用 field_alias 參數。

最佳實踐

  1. 統一別名策略:若專案需支援多種命名風格,建議在 Config 中使用 alias_generator,避免每個欄位手寫別名。
  2. 區分「輸入」與「輸出」需求:僅在需要的方向設定別名(例如只接受 camelCase、但回傳仍用 snake_case),可透過 json_encodersresponse_model_by_alias 控制。
  3. 保持模型純粹:別名只負責映射,不應混入業務邏輯;如需額外驗證,請使用 validator
  4. 測試雙向映射:寫單元測試確保 Model(**payload)model.dict(by_alias=True) 皆符合預期。
  5. 文件化 API 合約:在 OpenAPI 產出文件中,別名會自動顯示為鍵名,確保前端開發者看到的是正確的欄位名稱。

實際應用場景

場景 為何需要別名 實作要點
與第三方支付平台整合 支付平台的回傳 JSON 使用 transactionIdorderAmount 等 camelCase 鍵名。 使用 alias_generator 或個別 Field(alias=…),同時開啟 allow_population_by_field_name,方便在內部使用 snake_case。
舊有前端系統升級 老系統仍以 user-id(kebab-case)傳遞資料,新後端想改用 user_id user_id 欄位設定 alias="user-id",舊前端無需改動,未來可逐步遷移。
多語系 API 不同語系的前端可能使用不同命名慣例(中文拼音、英文 camelCase)。 建立多個別名映射,或在 alias_generator 中根據請求 Header 動態決定別名規則(需要自訂解析器)。
資料庫 ORM 與 API 分離 ORM 模型使用 snake_case,API 合約要求 camelCase。 在 Response Model 使用 alias_generator,讓 ORM 直接傳入 dict,FastAPI 會自動轉換。
微服務間協定 某微服務已固定使用 snake_case,但新服務想統一使用 camelCase 只在 API 層(FastAPI)設定別名,內部服務仍保持原有命名,降低改動成本。

總結

  • 欄位別名(alias) 是 Pydantic 與 FastAPI 連結外部 JSON 合約的關鍵工具,讓我們在保持 Pythonic 命名風格的同時,仍能兼容各式前端或第三方系統的鍵名。
  • 透過 Field(..., alias="…")allow_population_by_field_name、以及 alias_generator,我們可以彈性設定單一欄位或全域的別名規則。
  • Request 端別名負責解析,Response 端別名負責序列化,兩者可分別控制,確保 API 合約一致性。
  • 常見的坑包括忘記開啟 allow_population_by_field_name、別名衝突以及回傳時未使用 by_alias=True,只要遵守 最佳實踐(統一策略、雙向測試、文件化),即可避免這些問題。
  • 真實專案中,別名常用於 第三方整合、舊版系統升級、微服務協定 等情境,讓後端開發者能專注於業務邏輯,而非鍵名的繁瑣轉換。

掌握了別名的使用方法後,你的 FastAPI 專案將更具彈性、可維護性也會大幅提升。祝開發順利,期待看到你在實務中靈活運用這項技巧!