FastAPI 教學:Pydantic 模型的欄位別名(alias)
簡介
在使用 FastAPI 建立 API 時,我們往往會以 Pydantic 模型作為 Request / Response 的資料結構。實務上,前端或第三方系統送來的 JSON 鍵名不一定能直接對應到我們在程式碼裡定義的屬性名稱,這時 欄位別名(alias) 就派上用場。
透過別名,我們可以:
- 保持程式碼的可讀性:使用符合 Python 命名慣例的屬性名稱(如
user_id) - 兼容外部規範:仍能接受或回傳符合外部 API 規範的鍵名(如
userId、user-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 中的 alias 與 allow_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 可以是
productId、productName、price。 - 輸出時會自動使用 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_config 與 serialization_alias,舊寫法可能失效。 |
讀官方遷移文件,將 Config 改寫為 model_config = ConfigDict(...),或使用 field_alias 參數。 |
最佳實踐
- 統一別名策略:若專案需支援多種命名風格,建議在
Config中使用alias_generator,避免每個欄位手寫別名。 - 區分「輸入」與「輸出」需求:僅在需要的方向設定別名(例如只接受
camelCase、但回傳仍用snake_case),可透過json_encoders或response_model_by_alias控制。 - 保持模型純粹:別名只負責映射,不應混入業務邏輯;如需額外驗證,請使用
validator。 - 測試雙向映射:寫單元測試確保
Model(**payload)與model.dict(by_alias=True)皆符合預期。 - 文件化 API 合約:在 OpenAPI 產出文件中,別名會自動顯示為鍵名,確保前端開發者看到的是正確的欄位名稱。
實際應用場景
| 場景 | 為何需要別名 | 實作要點 |
|---|---|---|
| 與第三方支付平台整合 | 支付平台的回傳 JSON 使用 transactionId、orderAmount 等 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 專案將更具彈性、可維護性也會大幅提升。祝開發順利,期待看到你在實務中靈活運用這項技巧!