Skip to content

回應模型 - 回傳型別

🌐 AI 與人類共同完成的翻譯

此翻譯由人類指導的 AI 完成。🤝

可能會有對原意的誤解,或讀起來不自然等問題。🤖

你可以透過協助我們更好地引導 AI LLM來改進此翻譯。

英文版

你可以在「路徑操作函式」的回傳型別上加上註解,宣告用於回應的型別。

你可以像在函式「參數」的輸入資料那樣使用型別註解,你可以使用 Pydantic 模型、list、dictionary、整數、布林等純量值。

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


class Item(BaseModel):
    name: str
    description: str | None = None
    price: float
    tax: float | None = None
    tags: list[str] = []


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


@app.get("/items/")
async def read_items() -> list[Item]:
    return [
        Item(name="Portal Gun", price=42.0),
        Item(name="Plumbus", price=32.0),
    ]

FastAPI 會使用這個回傳型別來:

  • 驗證回傳的資料。
    • 如果資料無效(例如缺少欄位),代表你的應用程式程式碼有問題,沒有回傳應該回傳的內容,FastAPI 會回傳伺服器錯誤,而不是回傳不正確的資料。如此你和你的用戶端都能確定會收到預期的資料與資料結構。
  • 在 OpenAPI 的「路徑操作」中為回應新增 JSON Schema。
    • 這會被自動文件使用。
    • 也會被自動用戶端程式碼產生工具使用。

但更重要的是:

  • 它會將輸出資料限制並過濾為回傳型別中定義的內容。
    • 這對安全性特別重要,下面會再看到更多細節。

response_model 參數

有些情況下,你需要或想要回傳的資料與你宣告的型別不完全相同。

例如,你可能想要回傳一個 dictionary 或資料庫物件,但把回應宣告為一個 Pydantic 模型。這樣 Pydantic 模型就會替你回傳的物件(例如 dictionary 或資料庫物件)處理所有的資料文件、驗證等。

如果你加了回傳型別註解,工具與編輯器會(正確地)抱怨你的函式回傳的型別(例如 dict)與你宣告的(例如 Pydantic 模型)不同。

在這些情況下,你可以使用「路徑操作裝飾器」參數 response_model,而不是函式的回傳型別。

你可以在任何「路徑操作」上使用 response_model 參數:

  • @app.get()
  • @app.post()
  • @app.put()
  • @app.delete()
  • 等等。
from typing import Any

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


class Item(BaseModel):
    name: str
    description: str | None = None
    price: float
    tax: float | None = None
    tags: list[str] = []


@app.post("/items/", response_model=Item)
async def create_item(item: Item) -> Any:
    return item


@app.get("/items/", response_model=list[Item])
async def read_items() -> Any:
    return [
        {"name": "Portal Gun", "price": 42.0},
        {"name": "Plumbus", "price": 32.0},
    ]

注意

注意 response_model 是「裝飾器」方法(getpost 等)的參數。不是你的「路徑操作函式」的參數(像其他參數與請求主體那樣)。

response_model 接受的型別與你在 Pydantic 模型欄位中宣告的相同,所以它可以是一個 Pydantic 模型,也可以是例如由 Pydantic 模型組成的 list,像是 List[Item]

FastAPI 會使用這個 response_model 來做所有的資料文件、驗證等,並且也會將輸出資料轉換與過濾為其型別宣告。

提示

如果你在編輯器、mypy 等中有嚴格型別檢查,你可以把函式回傳型別宣告為 Any

這樣你是在告訴編輯器你是刻意回傳任意型別。但 FastAPI 仍會用 response_model 做資料文件、驗證、過濾等。

response_model 優先權

如果同時宣告了回傳型別與 response_modelresponse_model 會有優先權並由 FastAPI 使用。

如此一來,即便你回傳的實際型別與回應模型不同,你仍可在函式上加上正確的型別註解,供編輯器與如 mypy 的工具使用。同時仍由 FastAPI 使用 response_model 做資料驗證、文件化等。

你也可以使用 response_model=None 來停用該「路徑操作」的回應模型產生;當你為不是有效 Pydantic 欄位的東西加上型別註解時,可能需要這麼做,你會在下方某節看到範例。

回傳與輸入相同的資料

這裡我們宣告一個 UserIn 模型,其中會包含明文密碼:

from fastapi import FastAPI
from pydantic import BaseModel, EmailStr

app = FastAPI()


class UserIn(BaseModel):
    username: str
    password: str
    email: EmailStr
    full_name: str | None = None


# Don't do this in production!
@app.post("/user/")
async def create_user(user: UserIn) -> UserIn:
    return user

說明

要使用 EmailStr,請先安裝 email-validator

請先建立一個虛擬環境、啟用它,然後安裝,例如:

$ pip install email-validator

或:

$ pip install "pydantic[email]"

而我們使用這個模型同時宣告輸入與輸出:

from fastapi import FastAPI
from pydantic import BaseModel, EmailStr

app = FastAPI()


class UserIn(BaseModel):
    username: str
    password: str
    email: EmailStr
    full_name: str | None = None


# Don't do this in production!
@app.post("/user/")
async def create_user(user: UserIn) -> UserIn:
    return user

現在,當瀏覽器建立一個帶有密碼的使用者時,API 會在回應中回傳相同的密碼。

在這個例子中可能不是問題,因為是同一個使用者送出該密碼。

但如果我們對其他「路徑操作」使用相同的模型,我們可能會把使用者密碼送給所有用戶端。

警告

除非你非常清楚所有影響並確定自己在做什麼,否則永遠不要儲存使用者的明文密碼,也不要像這樣在回應中傳送。

新增一個輸出模型

我們可以改為建立一個包含明文密碼的輸入模型,以及一個不含密碼的輸出模型:

from typing import Any

from fastapi import FastAPI
from pydantic import BaseModel, EmailStr

app = FastAPI()


class UserIn(BaseModel):
    username: str
    password: str
    email: EmailStr
    full_name: str | None = None


class UserOut(BaseModel):
    username: str
    email: EmailStr
    full_name: str | None = None


@app.post("/user/", response_model=UserOut)
async def create_user(user: UserIn) -> Any:
    return user

在這裡,雖然「路徑操作函式」回傳的是同一個包含密碼的輸入使用者:

from typing import Any

from fastapi import FastAPI
from pydantic import BaseModel, EmailStr

app = FastAPI()


class UserIn(BaseModel):
    username: str
    password: str
    email: EmailStr
    full_name: str | None = None


class UserOut(BaseModel):
    username: str
    email: EmailStr
    full_name: str | None = None


@app.post("/user/", response_model=UserOut)
async def create_user(user: UserIn) -> Any:
    return user

...我們把 response_model 宣告為不包含密碼的 UserOut 模型:

from typing import Any

from fastapi import FastAPI
from pydantic import BaseModel, EmailStr

app = FastAPI()


class UserIn(BaseModel):
    username: str
    password: str
    email: EmailStr
    full_name: str | None = None


class UserOut(BaseModel):
    username: str
    email: EmailStr
    full_name: str | None = None


@app.post("/user/", response_model=UserOut)
async def create_user(user: UserIn) -> Any:
    return user

因此,FastAPI 會負責(透過 Pydantic)過濾掉輸出模型中未宣告的所有資料。

response_model 或回傳型別

在這種情況下,因為兩個模型不同,如果我們把函式回傳型別註解為 UserOut,編輯器和工具會抱怨我們回傳了無效的型別,因為它們是不同的類別。

這就是為什麼在這個例子中我們必須在 response_model 參數中宣告它。

...但繼續往下讀看看如何克服這個問題。

回傳型別與資料過濾

讓我們延續前一個範例。我們想要用一種型別來註解函式,但實際上希望能夠從函式回傳包含更多資料的內容。

我們希望 FastAPI 仍然用回應模型來過濾資料。這樣即使函式回傳更多資料,回應中也只會包含回應模型中宣告的欄位。

在前一個例子中,因為類別不同,我們必須使用 response_model 參數。但這也代表我們失去了編輯器與工具對函式回傳型別的檢查支援。

不過在大多數需要這樣做的情況下,我們只是想要像這個例子一樣,讓模型過濾/移除部分資料。

在這些情況下,我們可以利用類別與繼承,搭配函式的型別註解,取得更好的編輯器與工具支援,同時仍能讓 FastAPI 做資料過濾。

from fastapi import FastAPI
from pydantic import BaseModel, EmailStr

app = FastAPI()


class BaseUser(BaseModel):
    username: str
    email: EmailStr
    full_name: str | None = None


class UserIn(BaseUser):
    password: str


@app.post("/user/")
async def create_user(user: UserIn) -> BaseUser:
    return user

這樣我們能得到工具支援,對於編輯器與 mypy 來說,這段程式碼在型別上是正確的,同時我們也能得到 FastAPI 的資料過濾。

這是怎麼運作的?來看一下。🤓

型別註解與工具支援

先看看編輯器、mypy 與其他工具會怎麼看這件事。

BaseUser 有基礎欄位。然後 UserIn 繼承自 BaseUser 並新增 password 欄位,因此它會包含兩個模型的所有欄位。

我們把函式回傳型別註解為 BaseUser,但實際上回傳的是 UserIn 實例。

編輯器、mypy 與其他工具不會抱怨,因為就型別學而言,UserInBaseUser 的子類別,這代表當預期任何 BaseUser 時,UserIn 是一個有效的型別。

FastAPI 的資料過濾

對 FastAPI 而言,它會查看回傳型別,並確保你回傳的內容只包含該型別中宣告的欄位。

FastAPI 在內部會搭配 Pydantic 做一些事情,來確保不會把類別繼承的那些規則直接用在回傳資料的過濾上,否則你可能會回傳比預期更多的資料。

如此,你就能同時擁有兩種好處:具備工具支援的型別註解,以及資料過濾。

在文件中查看

在自動文件中,你可以看到輸入模型與輸出模型各自都有自己的 JSON Schema:

而且兩個模型都會用在互動式 API 文件中:

其他回傳型別註解

有時你回傳的東西不是有效的 Pydantic 欄位,你仍會在函式上加上註解,只為了獲得工具(編輯器、mypy 等)提供的支援。

直接回傳 Response

最常見的情況是直接回傳 Response(在進階文件中稍後會解釋)

from fastapi import FastAPI, Response
from fastapi.responses import JSONResponse, RedirectResponse

app = FastAPI()


@app.get("/portal")
async def get_portal(teleport: bool = False) -> Response:
    if teleport:
        return RedirectResponse(url="https://www.youtube.com/watch?v=dQw4w9WgXcQ")
    return JSONResponse(content={"message": "Here's your interdimensional portal."})
🤓 Other versions and variants
from fastapi import FastAPI, Response
from fastapi.responses import JSONResponse, RedirectResponse

app = FastAPI()


@app.get("/portal")
async def get_portal(teleport: bool = False) -> Response:
    if teleport:
        return RedirectResponse(url="https://www.youtube.com/watch?v=dQw4w9WgXcQ")
    return JSONResponse(content={"message": "Here's your interdimensional portal."})

這個簡單情境會由 FastAPI 自動處理,因為回傳型別註解是 Response 類別(或其子類別)。

而工具也會滿意,因為 RedirectResponseJSONResponse 都是 Response 的子類別,所以型別註解是正確的。

註解為某個 Response 的子類別

你也可以在型別註解中使用 Response 的子類別:

from fastapi import FastAPI
from fastapi.responses import RedirectResponse

app = FastAPI()


@app.get("/teleport")
async def get_teleport() -> RedirectResponse:
    return RedirectResponse(url="https://www.youtube.com/watch?v=dQw4w9WgXcQ")
🤓 Other versions and variants
from fastapi import FastAPI
from fastapi.responses import RedirectResponse

app = FastAPI()


@app.get("/teleport")
async def get_teleport() -> RedirectResponse:
    return RedirectResponse(url="https://www.youtube.com/watch?v=dQw4w9WgXcQ")

這同樣可行,因為 RedirectResponseResponse 的子類別,而 FastAPI 會自動處理這種簡單情況。

無效的回傳型別註解

但當你回傳其他任意物件(例如資料庫物件),它不是有效的 Pydantic 型別,並且你在函式上也這樣註解時,FastAPI 會嘗試從該型別註解建立一個 Pydantic 回應模型,因而失敗。

如果你有像是多種型別的聯集,其中一個或多個不是有效的 Pydantic 型別,也會發生相同的事情,例如這個就會失敗 💥:

from fastapi import FastAPI, Response
from fastapi.responses import RedirectResponse

app = FastAPI()


@app.get("/portal")
async def get_portal(teleport: bool = False) -> Response | dict:
    if teleport:
        return RedirectResponse(url="https://www.youtube.com/watch?v=dQw4w9WgXcQ")
    return {"message": "Here's your interdimensional portal."}

...這會失敗,因為該型別註解不是 Pydantic 型別,且它也不只是一個單一的 Response 類別或其子類別,而是 Responsedict 的聯集(兩者任一)。

停用回應模型

延續上面的例子,你可能不想要 FastAPI 執行預設的資料驗證、文件化、過濾等動作。

但你可能仍想在函式上保留回傳型別註解,以獲得編輯器與型別檢查工具(例如 mypy)的支援。

這種情況下,你可以設定 response_model=None 來停用回應模型的產生:

from fastapi import FastAPI, Response
from fastapi.responses import RedirectResponse

app = FastAPI()


@app.get("/portal", response_model=None)
async def get_portal(teleport: bool = False) -> Response | dict:
    if teleport:
        return RedirectResponse(url="https://www.youtube.com/watch?v=dQw4w9WgXcQ")
    return {"message": "Here's your interdimensional portal."}

這會讓 FastAPI 略過回應模型的產生,如此你就能使用任何你需要的回傳型別註解,而不會影響你的 FastAPI 應用程式。🤓

回應模型編碼參數

你的回應模型可能有預設值,例如:

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


class Item(BaseModel):
    name: str
    description: str | None = None
    price: float
    tax: float = 10.5
    tags: list[str] = []


items = {
    "foo": {"name": "Foo", "price": 50.2},
    "bar": {"name": "Bar", "description": "The bartenders", "price": 62, "tax": 20.2},
    "baz": {"name": "Baz", "description": None, "price": 50.2, "tax": 10.5, "tags": []},
}


@app.get("/items/{item_id}", response_model=Item, response_model_exclude_unset=True)
async def read_item(item_id: str):
    return items[item_id]
  • description: Union[str, None] = None(或在 Python 3.10 中的 str | None = None)預設為 None
  • tax: float = 10.5 預設為 10.5
  • tags: List[str] = [] 預設為空的 list:[]

但如果這些值其實沒有被儲存,你可能想要在結果中省略它們。

例如,如果你在 NoSQL 資料庫中有包含許多選擇性屬性的模型,但你不想傳送充滿預設值的冗長 JSON 回應。

使用 response_model_exclude_unset 參數

你可以在「路徑操作裝飾器」上設定 response_model_exclude_unset=True

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


class Item(BaseModel):
    name: str
    description: str | None = None
    price: float
    tax: float = 10.5
    tags: list[str] = []


items = {
    "foo": {"name": "Foo", "price": 50.2},
    "bar": {"name": "Bar", "description": "The bartenders", "price": 62, "tax": 20.2},
    "baz": {"name": "Baz", "description": None, "price": 50.2, "tax": 10.5, "tags": []},
}


@app.get("/items/{item_id}", response_model=Item, response_model_exclude_unset=True)
async def read_item(item_id: str):
    return items[item_id]

如此這些預設值就不會被包含在回應中,只有實際被設定的值才會包含。

因此,如果你對該「路徑操作」發送針對 ID 為 foo 的項目的請求,回應(不包含預設值)會是:

{
    "name": "Foo",
    "price": 50.2
}

說明

你也可以使用:

  • response_model_exclude_defaults=True
  • response_model_exclude_none=True

Pydantic 文件中對 exclude_defaultsexclude_none 的說明。

對於有預設值欄位也有實際值的資料

但如果你的資料在模型中對於有預設值的欄位也有實際值,例如 ID 為 bar 的項目:

{
    "name": "Bar",
    "description": "The bartenders",
    "price": 62,
    "tax": 20.2
}

它們會被包含在回應中。

與預設值相同的資料

如果資料的值與預設值相同,例如 ID 為 baz 的項目:

{
    "name": "Baz",
    "description": None,
    "price": 50.2,
    "tax": 10.5,
    "tags": []
}

FastAPI 足夠聰明(其實是 Pydantic 足夠聰明)去判斷,儘管 descriptiontaxtags 的值與預設值相同,但它們是被明確設定的(而不是取自預設值)。

因此,它們會被包含在 JSON 回應中。

提示

注意預設值可以是任何東西,不只有 None

它們可以是一個 list([])、一個 float10.5,等等。

response_model_includeresponse_model_exclude

你也可以使用「路徑操作裝飾器」參數 response_model_includeresponse_model_exclude

它們接受一個由屬性名稱字串所組成的 set,分別用來包含(省略其他)或排除(包含其他)屬性。

如果你只有一個 Pydantic 模型並且想從輸出移除部分資料,這可以作為一個快速捷徑。

提示

但仍建議使用上面提到的作法,使用多個類別,而不是這些參數。

因為在你的應用程式 OpenAPI(與文件)中所產生的 JSON Schema 仍會是完整模型的,即便你使用 response_model_includeresponse_model_exclude 省略了一些屬性。

response_model_by_alias 也有類似的情況。

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


class Item(BaseModel):
    name: str
    description: str | None = None
    price: float
    tax: float = 10.5


items = {
    "foo": {"name": "Foo", "price": 50.2},
    "bar": {"name": "Bar", "description": "The Bar fighters", "price": 62, "tax": 20.2},
    "baz": {
        "name": "Baz",
        "description": "There goes my baz",
        "price": 50.2,
        "tax": 10.5,
    },
}


@app.get(
    "/items/{item_id}/name",
    response_model=Item,
    response_model_include={"name", "description"},
)
async def read_item_name(item_id: str):
    return items[item_id]


@app.get("/items/{item_id}/public", response_model=Item, response_model_exclude={"tax"})
async def read_item_public_data(item_id: str):
    return items[item_id]

提示

語法 {"name", "description"} 會建立一個包含這兩個值的 set

它等同於 set(["name", "description"])

使用 list 來代替 set

如果你忘了使用 set 而用了 listtuple,FastAPI 仍會把它轉換成 set,並能正確運作:

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


class Item(BaseModel):
    name: str
    description: str | None = None
    price: float
    tax: float = 10.5


items = {
    "foo": {"name": "Foo", "price": 50.2},
    "bar": {"name": "Bar", "description": "The Bar fighters", "price": 62, "tax": 20.2},
    "baz": {
        "name": "Baz",
        "description": "There goes my baz",
        "price": 50.2,
        "tax": 10.5,
    },
}


@app.get(
    "/items/{item_id}/name",
    response_model=Item,
    response_model_include=["name", "description"],
)
async def read_item_name(item_id: str):
    return items[item_id]


@app.get("/items/{item_id}/public", response_model=Item, response_model_exclude=["tax"])
async def read_item_public_data(item_id: str):
    return items[item_id]

重點回顧

使用「路徑操作裝飾器」的 response_model 參數來定義回應模型,特別是為了確保私有資料被過濾掉。

使用 response_model_exclude_unset 僅回傳被明確設定的值。