本文 AI 產出,尚未審核

FastAPI 資料驗證與轉換(Validation & Serialization)

主題:schema_extra 額外資訊


簡介

在使用 FastAPI 建立 API 時,我們常會依賴 Pydantic 來定義資料模型(Model),藉此完成請求參數的驗證與回傳資料的序列化。除了基本的欄位型別與驗證規則外,FastAPI 會自動根據模型產生 OpenAPI 文件,讓前端或第三方開發者能即時取得 API 規格。

然而,僅靠欄位說明與預設值,有時無法完整呈現實務上需要的範例或額外說明。這時,schema_extra 便是 Pydantic 提供的強大擴充機制,讓我們可以在模型的 JSON Schema 中加入自訂的 example、description、default 等資訊,直接影響產出的 Swagger UI 與 Redoc 文件。

掌握 schema_extra 的使用方式,不僅能讓 API 文件更具可讀性,也能在開發與除錯階段提供即時的範例,提升團隊協作效率。接下來,我們將一步步解說 schema_extra 的概念、實作方式與最佳實踐,並提供多個可直接套用的範例。


核心概念

什麼是 schema_extra

schema_extraPydantic 模型(BaseModel)的 Config 子類別屬性之一。它接受一個字典,該字典會在模型轉換成 JSON Schema 時被合併進去。常見的用途包括:

用途 在 OpenAPI 中的呈現
example Swagger UI 的範例資料
description 欄位或模型的說明文字
default 預設值的顯示(不等同於模型層面的 default
title 模型或欄位的標題

重點schema_extra 僅影響產生的 JSON Schema,不會改變 Pydantic 本身的驗證行為。

基本語法

from pydantic import BaseModel

class User(BaseModel):
    id: int
    name: str
    email: str

    class Config:
        # 這裡放入自訂的 JSON Schema 片段
        schema_extra = {
            "example": {
                "id": 1,
                "name": "Alice",
                "email": "alice@example.com"
            },
            "description": "使用者資訊模型"
        }

在上述程式碼中,example 會在 Swagger UI 的 Request bodyResponse model 區塊顯示,而 description 則會成為模型的說明文字。

常見的 schema_extra 結構

  1. 在模型層級加入 example

    schema_extra = {
        "example": { ... }
    }
    
  2. 在欄位層級加入 exampledescription

    schema_extra = {
        "properties": {
            "name": {
                "title": "使用者名稱",
                "description": "使用者的全名",
                "example": "Bob"
            }
        }
    }
    
  3. 同時提供多個範例(OpenAPI 3.0+)

    schema_extra = {
        "examples": {
            "basic": {
                "summary": "最簡單的範例",
                "value": {"id": 2, "name": "Carol", "email": "carol@example.com"}
            },
            "complex": {
                "summary": "含有額外欄位的範例",
                "value": {"id": 3, "name": "Dave", "email": "dave@example.com", "age": 30}
            }
        }
    }
    

程式碼範例

以下提供 5 個實用範例,涵蓋最常見的需求與一些進階技巧。每個範例均附上說明,方便讀者快速理解。

範例 1:最基本的模型範例(單一 example

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

class Item(BaseModel):
    name: str
    price: float
    is_offer: bool | None = None

    class Config:
        # 為整個模型提供一個範例,Swagger UI 會直接顯示
        schema_extra = {
            "example": {
                "name": "Apple iPhone 15",
                "price": 29999.0,
                "is_offer": True
            }
        }

@app.post("/items/")
async def create_item(item: Item):
    return item

說明:當使用者在 Swagger UI 點擊 Try it out 時,請求的 Body 欄位會自動填入 example 中的資料,減少手動輸入的時間。

範例 2:欄位層級的說明與範例

class Product(BaseModel):
    id: int
    title: str
    description: str | None = None
    price: float

    class Config:
        schema_extra = {
            "properties": {
                "id": {
                    "title": "商品編號",
                    "description": "系統自動產生的唯一識別碼",
                    "example": 1001
                },
                "title": {
                    "title": "商品名稱",
                    "description": "顧客在前端看到的商品標題",
                    "example": "無線藍牙耳機"
                },
                "price": {
                    "title": "商品價格",
                    "description": "以新台幣為單位,含稅",
                    "example": 1999.5
                }
            }
        }

說明properties 內的每個欄位都可以自行設定 titledescriptionexample,讓文件更具可讀性。

範例 3:提供多個範例(OpenAPI 3.0+)

class Order(BaseModel):
    order_id: str
    items: list[Item]
    total: float

    class Config:
        schema_extra = {
            "examples": {
                "single_item": {
                    "summary": "單一商品的訂單",
                    "value": {
                        "order_id": "ORD001",
                        "items": [
                            {"name": "USB-C 充電線", "price": 299.0, "is_offer": None}
                        ],
                        "total": 299.0
                    }
                },
                "multiple_items": {
                    "summary": "多商品訂單",
                    "value": {
                        "order_id": "ORD002",
                        "items": [
                            {"name": "鍵盤", "price": 1200.0, "is_offer": False},
                            {"name": "滑鼠", "price": 650.0, "is_offer": True}
                        ],
                        "total": 1850.0
                    }
                }
            }
        }

說明:在 Swagger UI 中,使用者可以從 Example Value 下拉選單切換不同範例,對測試 API 的情境更靈活。

範例 4:結合 descriptionexample,說明「可選欄位」的行為

class BlogPost(BaseModel):
    title: str
    content: str
    tags: list[str] | None = None   # 可選欄位

    class Config:
        schema_extra = {
            "example": {
                "title": "FastAPI 入門教學",
                "content": "本文將說明如何快速上手 FastAPI...",
                "tags": ["FastAPI", "Python", "Web"]
            },
            "properties": {
                "tags": {
                    "description": "文章標籤,若未提供則視為無標籤",
                    "example": ["API", "Tutorial"]
                }
            }
        }

說明tags 為可選欄位,透過 description 說明「若未提供則視為無標籤」的行為,讓前端開發者不會誤以為此欄位是必填。

範例 5:在 response_model 中使用 schema_extra,自訂回傳範例

from fastapi import FastAPI, status
from fastapi.responses import JSONResponse

app = FastAPI()

class LoginResponse(BaseModel):
    access_token: str
    token_type: str = "bearer"

    class Config:
        schema_extra = {
            "example": {
                "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
                "token_type": "bearer"
            }
        }

@app.post("/login", response_model=LoginResponse, status_code=status.HTTP_200_OK)
async def login(username: str, password: str):
    # 假設驗證成功後產生 JWT
    token = "dummy_jwt_token"
    return JSONResponse(content={"access_token": token, "token_type": "bearer"})

說明:即使回傳是手動建立的 JSONResponse,只要在路由上指定 response_model,Swagger UI 仍會顯示 LoginResponseexample,協助前端了解回傳結構。


常見陷阱與最佳實踐

陷阱 說明 解決方式
schema_extra 覆寫了自動產生的欄位資訊 若在 properties 中只寫了部份欄位,未列出的欄位會失去自動產生的 descriptiontype 等資訊。 只在需要額外資訊的欄位上添加,其餘欄位保持空白或使用 ** 合併字典方式。
範例資料與實際驗證不一致 example 只是一個說明,FastAPI 不會根據它進行驗證。若範例格式錯誤,文件會誤導使用者。 確保 example 與模型驗證規則相符(例如型別、必填欄位)。可在開發階段寫測試檢查 example 的正確性。
過度使用 schema_extra 造成文件臃腫 大量的範例或說明會讓 Swagger UI 加載變慢,且難以維護。 只為關鍵模型或公共 API 添加,其餘可使用全局 descriptiontags 來說明。
忘記在 Config 中加入 orm_mode = True 若模型同時用於 ORM(如 SQLAlchemy)與 API 回傳,缺少 orm_mode 會導致序列化錯誤。 Config 中同時設定 orm_mode = Trueschema_extra
class Config: orm_mode = True; schema_extra = {...}
使用 example 時忽略 examples 單一 example 只能提供一個範例,無法展示多種使用情境。 若 API 有多種輸入情境,使用 examples(OpenAPI 3.0+)提供 多個範例

最佳實踐

  1. 保持範例與驗證同步

    • 建議在模型檔案最上方寫下 # fmt: off,使用 IDE 快速產生 example,再手動檢查。
  2. 分層管理說明

    • 模型層:使用 descriptiontitle 交代整體概念。
    • 欄位層:使用 properties 針對每個欄位補充說明與範例。
  3. 在專案的共用基底模型中統一設定

    • 建立 BaseSchema 繼承 BaseModel,在其中加入 orm_mode = True 與通用 schema_extra(如全局的 example 標記),子模型只需要覆寫差異部分。
  4. 自動化測試 example

    from pydantic import BaseModel
    import json
    
    def test_example_is_valid():
        example = MyModel.schema()["example"]
        # 若 example 為 None,測試失敗
        assert example is not None
        # 嘗試用 example 建立模型,確保驗證通過
        MyModel(**example)
    
    • 這樣可避免因範例錯誤而產生的文件誤導。

實際應用場景

場景 為何需要 schema_extra 範例
前端開發者快速測試 API 直接在 Swagger UI 中看到完整的請求範例,減少手動組裝 JSON 的時間。 透過 exampleexamples 提供「登入」與「註冊」的範例。
第三方合作夥伴 合作夥伴只看文件,若文件中有具體範例,可快速完成對接測試。 在付款 API 中加入 example,示範成功/失敗的回傳結構。
自動產生 API 客戶端 OpenAPI 生成的 SDK 會使用 example 產生測試代碼。 使用 openapi-generator 產生的 TypeScript 客戶端會自動帶入範例。
內部測試環境 CI/CD 流程可利用 example 直接產生測試資料,驗證 API 正確性。 在測試腳本中讀取 MyModel.schema()["example"] 作為測試 payload。
文件維護 隨著模型變更,schema_extra 直接寫在模型檔案中,文件自動同步更新。 當新增 age 欄位時,只需在 schema_extra["properties"]["age"] 補上說明與範例。

總結

  • schema_extraPydantic 為 OpenAPI 文件提供自訂資訊的關鍵工具。
  • 透過 exampleexamplesproperties 等屬性,我們可以在 Swagger UI 中呈現完整、正確且易於理解的範例,提升前後端協作效率。
  • 使用時要注意 範例與驗證的一致性避免過度堆砌資訊,並建議 加入自動化測試 以確保文件永遠保持正確。
  • 在實務上,schema_extra 幫助前端快速測試、第三方合作、SDK 生成與 CI/CD 測試等多種情境,是真正讓 API 可用、可測、可維護 的加分項。

掌握了 schema_extra 的寫法與最佳實踐後,你的 FastAPI 專案將不再只是功能完整的後端服務,更會是一套 清晰、可自說明的 API 生態系統。祝開發順利!