跳转至

响应模型 - 返回类型

🌐 Translation by AI and humans

This translation was made by AI guided by humans. 🤝

It could have mistakes of misunderstanding the original meaning, or looking unnatural, etc. 🤖

You can improve this translation by helping us guide the AI LLM better.

English version

你可以通过为路径操作函数返回类型添加注解来声明用于响应的类型。

和为输入数据在函数参数里做类型注解的方式相同,你可以使用 Pydantic 模型、listdict、以及整数、布尔值等标量类型。

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 参数

在一些情况下,你需要或希望返回的数据与声明的类型不完全一致。

例如,你可能希望返回一个字典或数据库对象,但将其声明为一个 Pydantic 模型。这样 Pydantic 模型就会为你返回的对象(例如字典或数据库对象)完成文档、校验等工作。

如果你添加了返回类型注解,工具和编辑器会(正确地)报错,提示你的函数返回的类型(例如 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] = [] 默认值为一个空列表:[]

但如果它们并没有被实际存储,你可能希望在结果中省略这些默认值。

例如,当你在 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

它们可以是一个列表([])、值为 10.5float,等等。

response_model_includeresponse_model_exclude

你还可以使用路径操作装饰器response_model_includeresponse_model_exclude 参数。

它们接收一个由属性名 str 组成的 set,用于包含(忽略其他)或排除(包含其他)这些属性。

当你只有一个 Pydantic 模型,并且想要从输出中移除一些数据时,这可以作为一种快捷方式。

提示

但仍然推荐使用上面的思路,使用多个类,而不是这些参数。

因为即使你使用 response_model_includeresponse_model_exclude 省略了一些属性,你的应用在 OpenAPI(和文档)中生成的 JSON Schema 仍然会是完整模型。

这同样适用于类似的 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 来仅返回显式设置的值。