FastAPI 路由(Routing) ── Response Model 自動轉換
簡介
在 RESTful API 開發中,回傳資料的結構 直接影響前端與其他服務的整合成本。若每一次都手動組裝 JSON,既容易出錯,也會讓程式碼變得雜亂。FastAPI 內建的 Response Model 機制,讓我們只需要定義 Python 的資料模型(通常使用 Pydantic),框架就會自動把回傳值 序列化 成符合模型的 JSON,並在開發階段提供型別檢查與自動文件產生。
本篇文章將說明什麼是 Response Model、它如何在路由中自動轉換、常見的使用情境與陷阱,並提供多個實作範例,協助你在專案中快速、正確地使用這項功能。
核心概念
1. 為什麼需要 Response Model?
| 需求 | 若不使用 Response Model | 使用 Response Model 的好處 |
|---|---|---|
| 一致性 | 每個 endpoint 必須自行確保回傳欄位、型別正確 | 統一的模型保證所有回傳都遵循同一規範 |
| 型別安全 | 只能靠測試或手動檢查 | Pydantic 於執行時即檢查資料型別,錯誤會拋出 ValidationError |
| 自動文件 | 需要自行撰寫 OpenAPI/Swagger | FastAPI 會根據模型自動產生文件,前端即能即時看到正確格式 |
| 維護成本 | 改動欄位需要在多處修改 | 只改模型即可,所有使用該模型的 endpoint 自動更新 |
2. 基本語法
在 FastAPI 中,路由的 @app.get/post/... 裝飾器接受 response_model 參數:
@app.get("/items/{item_id}", response_model=ItemResponse)
async def read_item(item_id: int):
# 取得資料庫資料或其他來源
item = get_item_from_db(item_id)
return item # 只要回傳 dict、Pydantic model 或相容物件即可
ItemResponse必須是 Pydantic 的BaseModel子類別。- FastAPI 會在回傳前 自動執行序列化,只保留模型中定義的欄位,其他多餘欄位會被剔除(除非設定
response_model_exclude_unset=False)。
3. Pydantic Model 的基本寫法
from pydantic import BaseModel, Field
from typing import List, Optional
class ItemResponse(BaseModel):
id: int = Field(..., description="商品唯一識別碼")
name: str = Field(..., max_length=100, description="商品名稱")
price: float = Field(..., gt=0, description="商品價格")
tags: Optional[List[str]] = Field(default=None, description="商品標籤")
Field用來描述欄位的驗證規則與說明,會直接映射到 OpenAPI 文件。Optional表示此欄位可為null或缺省。
4. Response Model 的自動轉換流程
- 路由函式回傳:可以是
dict、list、Pydantic model、ORM 物件(只要可被jsonable_encoder處理)。 - FastAPI 觸發
response_model:將回傳值傳給 Pydantic 的parse_obj_as,進行 資料驗證 與 欄位過濾。 - 序列化:使用
jsonable_encoder把資料轉為 JSON 可序列化的型別(如datetime會自動轉為 ISO8601 字串)。 - 回傳:最終的 HTTP 回應只包含模型中定義的欄位,並以 application/json 為 Content-Type。
程式碼範例
以下提供 5 個實用範例,涵蓋常見需求與技巧。
範例 1️⃣ 基本回傳模型
# file: main.py
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class HelloResponse(BaseModel):
message: str
version: str = "1.0"
@app.get("/hello", response_model=HelloResponse)
async def say_hello():
# 直接回傳 dict,FastAPI 會自動轉成 HelloResponse
return {"message": "Hello, FastAPI!"}
說明:即使回傳的 dict 少了一個
version欄位,Pydantic 仍會使用模型的預設值填補。
範例 2️⃣ 回傳 ORM 物件(SQLAlchemy)
# file: models.py
from sqlalchemy import Column, Integer, String
from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
username = Column(String, unique=True, index=True)
email = Column(String, unique=True, index=True)
# file: schemas.py
from pydantic import BaseModel
class UserResponse(BaseModel):
id: int
username: str
email: str
class Config:
orm_mode = True # 讓 Pydantic 能從 ORM 物件讀取屬性
# file: main.py (續)
from fastapi import Depends, HTTPException
from sqlalchemy.orm import Session
from . import models, schemas, database
@app.get("/users/{user_id}", response_model=schemas.UserResponse)
def read_user(user_id: int, db: Session = Depends(database.get_db)):
user = db.query(models.User).filter(models.User.id == user_id).first()
if not user:
raise HTTPException(status_code=404, detail="User not found")
return user # 直接回傳 ORM 物件,因為 orm_mode=True 可自動轉換
註解:
orm_mode = True讓 Pydantic 允許從非 dict 物件(如 SQLAlchemy 的模型)讀取屬性,這是使用資料庫模型時的必備設定。
範例 3️⃣ 回傳子集合與排除欄位
class Product(BaseModel):
id: int
name: str
cost_price: float
sale_price: float
class Config:
orm_mode = True
# 只想回傳給前端 id、name、sale_price,不要曝光成本價
@app.get("/products/{pid}", response_model=Product, response_model_exclude={"cost_price"})
def get_product(pid: int):
product = fetch_product_from_service(pid) # 回傳 Product ORM 物件
return product
說明:
response_model_exclude可以在回傳時排除特定欄位,避免敏感資訊外洩。
範例 4️⃣ 動態欄位(include / exclude)
from fastapi import Query
@app.get("/items/{item_id}", response_model=ItemResponse)
def read_item(
item_id: int,
fields: Optional[str] = Query(None, description="用逗號分隔的欄位清單")
):
item = get_item_from_db(item_id)
if fields:
include_set = set(fields.split(","))
return item.dict(include=include_set) # 只回傳指定欄位
return item
技巧:利用
dict(include=...)或dict(exclude=...)搭配response_model,可以在同一個 endpoint 實作 可自訂欄位的 API,減少前端多餘的資料傳輸。
範例 5️⃣ 自訂序列化(jsonable_encoder)
from fastapi.responses import JSONResponse
from fastapi.encoders import jsonable_encoder
from datetime import datetime
class EventResponse(BaseModel):
id: int
name: str
start_time: datetime
@app.get("/events/{eid}", response_model=EventResponse)
def get_event(eid: int):
event = {
"id": eid,
"name": "開發者大會",
"start_time": datetime(2025, 12, 1, 9, 30) # datetime 需要被編碼
}
# 若要自行控制回傳格式,可先走 jsonable_encoder
json_compatible = jsonable_encoder(event)
return JSONResponse(content=json_compatible)
重點:
jsonable_encoder會把datetime、UUID、Decimal等非 JSON 原生型別轉成字串或數字,確保回傳的 JSON 合法。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方案 / Best Practice |
|---|---|---|
| 模型與實際回傳不一致 | 回傳的 dict 包含模型未定義的欄位,卻未使用 response_model_exclude_unset 或 exclude,導致意外資訊外洩。 |
使用 response_model_exclude_unset=True 或明確設定 exclude/include。 |
orm_mode 忘記設定 |
從 ORM 物件直接回傳卻未開啟 orm_mode,會拋出 ValueError: Object of type <Model> is not JSON serializable。 |
在所有需要從 ORM 讀取的模型裡加上 class Config: orm_mode = True。 |
| 大量資料一次回傳 | response_model 會對每筆資料進行驗證,若列表過大會造成效能瓶頸。 |
使用分頁(limit/offset)或 StreamingResponse;對列表使用 response_model=List[ItemResponse] 並搭配 response_model_exclude_unset=True 減少驗證負擔。 |
自訂 jsonable_encoder 與模型衝突 |
手動編碼後再交給 FastAPI 仍套用 response_model,可能導致重複序列化或欄位遺失。 |
若已自行編碼,直接回傳 JSONResponse,或在 response_model 中使用 exclude_unset 來避免重複處理。 |
預設值與 None 混用 |
欄位設 Optional[...] = None,但在回傳時忘記提供值,會出現 null,前端若未處理會出錯。 |
明確在模型中設定 default 或在路由內使用 if value is None: del data["field"],或在 response_model_exclude_none=True。 |
最佳實踐
- 模型即文件:所有公共 API 都應該有對應的 Pydantic
BaseModel,不僅方便驗證,也能自動產生 Swagger。 - 最小化回傳:只回傳前端需要的欄位,使用
exclude或include,減少網路流量。 - 統一錯誤格式:自訂全域例外處理器,將錯誤訊息包裝成符合模型的 JSON,保持 API 一致性。
- 測試模型:在單元測試裡直接呼叫
schema.parse_obj(),確保模型本身的驗證規則正確。 - 分層設計:將資料庫模型、業務邏輯、API 輸入/輸出模型分離,避免模型耦合造成維護困難。
實際應用場景
電商平台的商品 API
- 商品資料庫表格包含成本價、庫存、供應商資訊。對外只需要回傳
id、name、sale_price、image_url。使用response_model_exclude={"cost_price","supplier_info"},確保敏感資料不外洩。
- 商品資料庫表格包含成本價、庫存、供應商資訊。對外只需要回傳
即時聊天系統的訊息列表
- 每筆訊息包含
id、sender_id、content、created_at、deleted_at。前端只需要顯示未刪除的訊息,且不需要deleted_at。在路由中使用response_model=MessageOut, response_model_exclude={"deleted_at"},同時搭配limit/offset分頁。
- 每筆訊息包含
金融服務的交易紀錄
- 交易模型包含
transaction_id、amount、currency、status、internal_notes。內部備註不應給客戶,利用response_model_exclude={"internal_notes"},並在測試環境開啟response_model_include_unset=False以驗證欄位完整性。
- 交易模型包含
多語系的說明文件 API
- 使用
Optional[Dict[str, str]]讓前端自行決定取哪個語系的文字,回傳時只保留title、content,其餘欄位使用exclude_none=True避免傳空值。
- 使用
總結
- Response Model 是 FastAPI 提供的「模型即文件、模型即驗證」的核心功能,讓 API 回傳結構化、可預測且安全。
- 只要定義好 Pydantic BaseModel,再於路由加上
response_model=YourModel,框架會自動完成 資料驗證、欄位過濾、序列化,同時產生完整的 OpenAPI 文件。 - 在實務開發中,配合
orm_mode、exclude/include、exclude_unset、exclude_none等參數,可靈活控制回傳內容,避免敏感資訊外洩與過度傳輸。 - 常見的陷阱包括忘記設定
orm_mode、未排除不必要欄位、一次回傳過大資料等,透過 最佳實踐(模型即文件、最小化回傳、分層設計、全域錯誤處理)可以有效降低風險。
掌握了 Response Model 的自動轉換 後,你的 FastAPI 專案將變得更乾淨、可維護,同時提供給前端與合作夥伴一致且可靠的 API 介面。祝開發順利,Happy coding!