FastAPI
單元:Query / Path / Body 參數處理
主題:Form 資料解析(Form())
簡介
在 Web 開發中,表單 (Form) 是最常見的資料傳遞方式之一。使用者在瀏覽器上填寫文字、下拉選單或檔案,最終會以 application/x-www-form-urlencoded(或 multipart/form-data)的形式送到伺服器。若你在建構 API 時只熟悉 query、path、body(JSON)三種參數類型,面對傳統 HTML 表單時就會感到手足無措。
FastAPI 為了保持 一致且型別安全 的開發體驗,提供了 Form() 這個依賴項(dependency)來解析表單資料。只要在路由函式的參數前加上 Form(),FastAPI 會自動把 request.form() 的內容轉換成相對應的 Python 型別,並且在 OpenAPI 文件中正確呈現。
本篇文章將從 概念、實作範例、常見陷阱、最佳實踐,到 實務應用,一步步帶你熟悉 FastAPI 的表單解析,讓你能在開發 CRUD、登入、檔案上傳等功能時,得心應手。
核心概念
1. Form() 是什麼?
Form()本質上是一個 依賴注入 (Dependency Injection),它告訴 FastAPI:「這個參數的來源是 HTTP 表單」。- 它只能用在
POST、PUT、PATCH等會有 request body 的 HTTP 方法;若用在GET,FastAPI 會拋出錯誤。 - 解析後的資料會自動套用 Pydantic 型別驗證,例如
int、float、bool、datetime,甚至自訂的 Enum。
from fastapi import FastAPI, Form
app = FastAPI()
@app.post("/login")
async def login(username: str = Form(...), password: str = Form(...)):
return {"msg": f"Welcome, {username}!"}
Form(...)中的...表示 必填;若改成Form(None)或Form(default="guest"),則變為可選。
2. 為什麼要使用 Form() 而不是手動讀取 request.form()?
手動讀取 (await request.form()) |
使用 Form() |
|---|---|
| 需要自行解析、轉型、驗證 | 自動完成型別轉換與驗證 |
| 無法在 OpenAPI 中自動產生表單說明 | 自動產生 Swagger UI 的表單欄位 |
| 失去依賴注入的可測試性 | 可直接在測試函式中注入假資料 |
因此,建議在大多數情況下直接使用 Form(),只有在需要特殊處理(例如多檔案混合)時才考慮手動讀取。
3. Form() 與 File() 的關係
- 若表單同時包含 文字欄位 與 檔案上傳,需要同時使用
Form()(文字)與File()(檔案)。 File()內部同樣是依賴注入,回傳UploadFile物件,支援非同步讀取。
from fastapi import FastAPI, Form, File, UploadFile
@app.post("/profile")
async def create_profile(
name: str = Form(...),
age: int = Form(...),
avatar: UploadFile = File(...)
):
content = await avatar.read()
# 處理檔案內容...
return {"name": name, "age": age, "filename": avatar.filename}
4. 多值表單欄位(List)
HTML <select multiple> 或同名的 <input> 會送出 多個相同名稱的值。FastAPI 只要把參數型別寫成 list[<type>],就能自動收集所有值。
@app.post("/tags")
async def add_tags(tags: list[str] = Form(...)):
# tags 會是一個字串列表,例如 ["python", "fastapi"]
return {"tags": tags}
程式碼範例
以下提供 五個實用範例,涵蓋從最簡單的登入表單到結合檔案與多值欄位的進階情境。每段程式碼皆附上說明註解,方便你直接貼上測試。
範例 1:最基本的登入表單
# file: main.py
from fastapi import FastAPI, Form, HTTPException, status
app = FastAPI()
# 假資料庫
FAKE_USERS_DB = {"alice": "secret123", "bob": "password"}
@app.post("/login")
async def login(
username: str = Form(..., description="使用者名稱"),
password: str = Form(..., description="使用者密碼")
):
"""
透過 Form() 解析表單資料,並在資料庫中驗證使用者。
"""
stored_pwd = FAKE_USERS_DB.get(username)
if not stored_pwd or stored_pwd != password:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="帳號或密碼錯誤"
)
return {"msg": f"歡迎回來, {username}!"}
只要在 Swagger UI(
/docs)點擊 Try it out,即可看到自動產生的表單欄位。
範例 2:帶有預設值與可選欄位
@app.post("/register")
async def register(
username: str = Form(..., min_length=3, max_length=20),
password: str = Form(..., min_length=6),
email: str = Form(None, description="非必填,若提供則必須符合 email 格式"),
role: str = Form("user", description="預設角色為 user")
):
"""
示範如何設定必填、可選與預設值。
"""
# 這裡可以使用 Pydantic BaseModel 進一步驗證 email 格式
return {"username": username, "email": email, "role": role}
Form(None)代表欄位可為None(即未提供)。Form("user")為 預設值,若前端未送出此欄位,FastAPI 會自動填入"user"。
範例 3:文字欄位 + 檔案上傳
@app.post("/upload")
async def upload_file(
description: str = Form(..., max_length=100),
file: UploadFile = File(..., description="上傳的檔案")
):
"""
同時接收文字說明與檔案,示範 Form() 與 File() 的結合使用。
"""
# 讀取檔案內容(非同步)
content = await file.read()
size_kb = len(content) / 1024
return {
"filename": file.filename,
"content_type": file.content_type,
"size_kb": round(size_kb, 2),
"description": description
}
若使用
multipart/form-data,Swagger UI 會自動呈現檔案上傳 UI。
範例 4:多值欄位(Tag 系統)
@app.post("/posts")
async def create_post(
title: str = Form(...),
content: str = Form(...),
tags: list[str] = Form([]) # 預設空列表,允許不傳 tags
):
"""
使用 list[str] 解析多個相同名稱的表單欄位。
前端可以使用 <select multiple> 或多個同名 <input>.
"""
return {"title": title, "tags": tags}
前端範例(HTML)
<form action="/posts" method="post">
<input type="text" name="title" placeholder="標題" required>
<textarea name="content" placeholder="內容" required></textarea>
<select name="tags" multiple>
<option value="fastapi">FastAPI</option>
<option value="python">Python</option>
<option value="web">Web</option>
</select>
<button type="submit">送出</button>
</form>
範例 5:結合 Enum 與驗證
from enum import Enum
from pydantic import EmailStr
class Role(str, Enum):
admin = "admin"
moderator = "moderator"
user = "user"
@app.post("/admin/create")
async def create_user(
email: EmailStr = Form(..., description="使用者的 Email"),
role: Role = Form(..., description="使用者角色,必須是 enum 中的值")
):
"""
利用 Pydantic 的 EmailStr 與自訂 Enum,實現更嚴謹的驗證。
"""
return {"email": email, "role": role}
EmailStr會自動檢查 Email 格式。Enum使 Swagger UI 顯示下拉選單,避免前端自行硬編碼。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方案 |
|---|---|---|
忘記 await request.form() |
若自行讀取表單卻未使用 await,會得到 FormData 物件而非實際資料。 |
盡量使用 Form(),或在手動讀取時寫 await request.form()。 |
使用 GET 方法搭配 Form() |
Form() 只在有 request body 的方法中有效。 |
改用 query 參數,或改為 POST。 |
多檔案上傳時只寫 File() |
若同時有文字欄位卻未使用 Form(),Swagger UI 會缺少文字欄位。 |
同時宣告 Form() 與 File(),如範例 3。 |
| List[Type] 沒有預設值 | 若前端未傳任何同名欄位,FastAPI 會拋出 422 錯誤。 | 為 List 參數提供預設值 Form([]) 或 Form(None)。 |
| 資料驗證不完整 | 只依賴 Form() 會執行基本型別驗證,對 Email、URL 等需求不足。 |
結合 Pydantic 型別(如 EmailStr、HttpUrl)或自行寫 validator。 |
最佳實踐
- 明確宣告必填與預設值:使用
Form(...)(必填)或Form(default)(可選),讓 OpenAPI 文件更清晰。 - 利用 Pydantic 型別:如
EmailStr、conint(gt=0, lt=100),在表單層即完成驗證,減少後端檢查。 - 分離驗證與商業邏輯:若驗證較複雜,可建立 Pydantic BaseModel,再在路由中
Depends()注入。 - 保持 Swagger UI 與前端同步:
Form()會自動產生 UI,確保前端開發者不必自行撰寫說明文件。 - 測試時使用
TestClient:FastAPI 的TestClient支援data=(表單)與files=(檔案)參數,方便單元測試。
from fastapi.testclient import TestClient
client = TestClient(app)
def test_login_success():
response = client.post(
"/login",
data={"username": "alice", "password": "secret123"} # 以表單形式送出
)
assert response.status_code == 200
assert response.json()["msg"] == "歡迎回來, alice!"
實際應用場景
| 場景 | 為什麼需要 Form() | 範例 API |
|---|---|---|
| 使用者登入 / 註冊 | 大多數前端框架預設以 application/x-www-form-urlencoded 送出表單資料 |
/login、/register |
| 內容管理系統 (CMS) 的文章發佈 | 文章標題、內容、分類、標籤往往同時以表單送出,且可能包含圖片上傳 | /posts、/upload |
| 管理後台的批次操作 | 例如一次選取多筆資料的 ids(list)並送出執行刪除 |
/batch-delete |
| 行動裝置 App 的表單提交 | 手機端常用 multipart/form-data 包含檔案與文字,FastAPI 可直接解析 |
/profile |
| 第三方系統整合(Webhooks) | 某些服務只支援表單 POST,需用 FastAPI 接收並轉換成內部模型 | /webhook/receiver |
透過 Form(),開發者可以 快速把傳統表單資料映射成 Python 型別,同時享受 FastAPI 自動產生的 API 文件與驗證機制,提升開發效率與程式品質。
總結
Form()是 FastAPI 為 HTML 表單 所設計的依賴注入工具,讓文字欄位、列表、Enum、甚至與File()混合使用都變得簡潔且安全。- 使用
Form()時,必須配合有 request body 的 HTTP 方法(如 POST),並適當設定必填、預設與型別驗證。 - 透過 Pydantic 型別、
Enum、list等結合,可在 API 文件中自動產生友善的 UI,減少前後端溝通成本。 - 常見陷阱包括忘記
await、錯誤的 HTTP 方法、List 欄位未給預設值等,只要遵循上面的 最佳實踐,即可避免。 - 在實務開發中,從 登入/註冊、內容發布、檔案上傳 到 批次操作,
Form()都是不可或缺的工具。
掌握了 Form() 之後,你就能在 FastAPI 中自如地處理任何傳統表單,並且同時受惠於 自動文件生成、型別安全 與 高效能 的特性。祝你開發順利,打造出更易維護、更具可讀性的 API!