首页
/ FastAPI 高频开发配方手册:数据安全过滤、OpenAPI 文档配置与 JSON 兼容编码

FastAPI 高频开发配方手册:数据安全过滤、OpenAPI 文档配置与 JSON 兼容编码

2026-09-07 12:48:05作者:凤尚柏Louis

本篇技术指南取自 FastAPI 文档中面向通用或高频问题的 "How-To - Recipes"(通用配方)页面(关联文档见 docs/hi/docs/how-to/general.md,其内容与英文源版 docs/en/docs/how-to/general.md 一致)。该页面是一份"索引 + 速查"式的导航页,将日常开发中反复遇到的十个问题分类指向对应教程。本文以这十个配方为骨架,逐一展开它们背后的配置参数、完整代码与底层实现,帮助你在实际项目中快速落地:如何只把该返回的数据返回出去(安全过滤)如何让 JSON 响应序列化更高效如何把自动生成的 OpenAPI 文档与交互式文档界面打磨得清晰、规范、可维护,以及如何把任意 Python 对象转成可直接落库或序列化的 JSON 兼容数据

这份"通用配方页"到底回答什么问题

在文档的 How-To 板块中,general.md 定位为高频问题的"菜谱"(Recipes)。与按章节循序渐进的教学文档不同,这里每一条都相对独立——如果你当前项目恰好踩中其中某个问题,直接查阅对应配方即可,无需通读整本教程。需要系统学习时,仍建议按 Tutorial - User Guide 从头阅读。

该页面汇总的十个配方可归纳为三大主题群,本文也据此组织:

问题群 配方 完整教程
响应数据 过滤返回数据、优化响应性能 Response Model - Return Type
OpenAPI 文档 UI tags、summary、description、response_description、deprecated Path Operation Configuration
数据转换与元数据 JSON 兼容编码、OpenAPI metadata、自定义 URL、Docs URL JSON Compatible EncoderMetadata and Docs URLs

配方一:如何确保"不多返回"数据 —— 响应模型与安全过滤

1. 只声明一个返回类型,就获得四重能力

这是 general.md 中第一个也是最重要的配方:确认不会返回超出应返回范围的数据。做法非常简单——给 path operation function 加上返回类型注解(return type annotation),例如声明返回 Item

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]:  # 也可以注解 list[Item]
    return [
        Item(name="Portal Gun", price=42.0),
        Item(name="Plumbus", price=32.0),
    ]

(完整示例见 docs_src/response_model/tutorial001_01_py310.py。)

根据 Response Model - Return Type 的说明,FastAPI 会利用这个返回类型做四件事:

  1. 校验返回数据:如果返回内容不匹配(例如缺少字段),说明是应用代码自身写错了,FastAPI 会返回服务器错误而不是把错误形状的数据发给客户端,从而保证客户端拿到的数据一定符合预期结构;
  2. 生成 JSON Schema:为这个 path operation 生成响应对应的 JSON Schema,写入 OpenAPI,供自动文档与自动生成客户端代码的工具使用;
  3. 序列化为 JSON:调用基于 Rust 核心的 Pydantic 完成 JSON 序列化(这一点官方文档明确指出,见响应模型教程中的序列化说明),这也是配方二"性能优化"的实现基础;
  4. 限制与过滤输出:只输出返回类型中声明的字段——这是安全层面的关键能力,官方文档将其单独强调为最重要的一条。

2. 什么时候改用 response_model 装饰器参数

返回类型注解存在一种限制:如果函数实际要返回一个 dict 或数据库对象,但你希望按 Pydantic 模型来校验与文档化,类型注解会让编辑器/静态检查器报错(返回类型与注解不一致)。此时应改用 path operation 装饰器参数 response_model

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

要点:

  • response_model 是装饰器方法(get/post/put/delete 等)的参数,不是 path operation function 的参数;
  • 它接受与 Pydantic 字段相同类型的声明,既可以是单个模型,也可以是 list[Item] 这类容器;
  • 若你同时声明了返回类型注解与 response_modelresponse_model 优先级更高
  • 若想为该 path operation 关闭响应模型处理,可用 response_model=None(比如返回注解里含有不是合法 Pydantic 字段的类型时);
  • 如果编辑器与 mypy 报类型错误,可将函数返回类型注解为 Any,让 FastAPI 仍然按 response_model 完成文档化、校验与过滤。

3. 输入模型与输出模型分离:经典安全范例

通用配方页里点出的"数据过滤 - 安全"最常见场景是:输入时包含敏感字段,输出时绝不能原样带出。看下面这个反例——输入模型里带着明文 password,响应直接返回同一个 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


# 不要在生产环境这么做!
@app.post("/user/")
async def create_user(user: UserIn) -> UserIn:
    return user

虽然"谁创建用户谁收到自己的密码"看起来问题不大,但只要把这个模型复用在其它接口上,就可能把每个用户的密码都发给所有客户端。安全修复方案是单独定义输出模型 UserOut,不包含 password 字段

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

(完整示例见 docs_src/response_model/tutorial003_py310.py。)

这里函数体仍然返回包含密码的 user,但因为声明了 response_model=UserOut,FastAPI 会(借助 Pydantic)自动过滤掉输出模型中未声明的所有数据——明文密码永远不会出现在响应里。

4. 更优雅的做法:用继承同时拿到类型检查与过滤

既然 UserInUserOut 是不同的类,把函数注解为 UserOut 依然会被编辑器/类型检查器报错。当你的核心需求只是"从同一个更完整的模型里剔除一部分字段"时,可以让两个模型继承同一个基类,再用返回类型注解而不是 response_model

  • BaseUser:存放公共字段;
  • UserIn:继承 BaseUser 并追加 password
  • 函数返回类型注解为 BaseUser,实际返回 UserIn 实例——由于 UserInBaseUser 的子类,类型工具认为这是合法返回值;
  • FastAPI 内部在处理返回数据过滤时不会沿用这一"继承关系放行"逻辑,而是严格只输出注解类型 BaseUser 声明的字段,从而实现"工具支持"与"数据过滤"兼得。

示例文件见 docs_src/response_model/tutorial003_01_py310.py。文档中称之为 "getting the best of both worlds"。

5. 控制默认值字段是否出现在响应里

如果你的响应模型字段带有默认值,例如:

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

(完整示例见 docs_src/response_model/tutorial004_py310.py。)

当数据库中只存了部分字段时,你可能不希望响应里塞满默认值(尤其是 NoSQL 场景下大量可空属性的模型)。这时可以在装饰器上传入以下编码参数:

  • response_model_exclude_unset=True:只返回显式设置过的字段,未显式赋值的字段即使有默认值也不出现在结果中;
  • response_model_exclude_defaults=True:排除值与默认值相同的字段;
  • response_model_exclude_none=True:排除值为 None 的字段。

文档里给出的演示数据非常直观:同一个模型下,请求 ID 为 foo 的条目(未显式设置默认字段)会得到 {"name": "Foo", "price": 50.2};而 ID 为 bar 的条目因为显式写了 descriptiontax,这些字段会照常输出;ID 为 baz 的条目即使 description=Nonetax=10.5tags=[] 恰好与默认值相等,由于它们是被显式写入的,Pydantic 仍会把它们包含进 JSON 响应。注意:默认值不限于 None,可以是列表 []、浮点数等任意值。

另外还有一组 "include / exclude" 快捷参数:

  • response_model_include:传入 set[str],只保留列出的属性名;
  • response_model_exclude:传入 set[str],剔除列出的属性名。

语法上 {"name", "description"} 等同于 set(["name", "description"]);即使误传了 listtuple,FastAPI 也会自动转换为 set。官方文档仍建议优先用前面"拆分多个模型/继承"的做法,因为 include/exclude 不会改变 OpenAPI 中生成的完整模型 JSON Schema(response_model_by_alias 也有类似限制)。

底层实现提示:这些 exclude_* 参数最终会传递到响应序列化逻辑中。FastAPI 路由层在把 response_content 转成 JSON 时调用 jsonable_encoder(...)(见 fastapi/routing.py),而 jsonable_encoder 本身就是把这些参数逐一透传给 Pydantic 模型(见下方配方七)。仓库中还有针对过滤行为的回归测试,例如 tests/test_response_model_data_filter.pytests/test_response_model_include_exclude.py

6. 边界情况:直接返回 Response、注解失效与关闭响应模型


配方二:返回 JSON 时如何优化响应性能

通用配方页给出的结论十分直接:当要返回 JSON 数据时,给 path operation 声明返回类型(return type)或使用 response_model,让 Pydantic 在 Rust 侧完成 JSON 序列化,避免数据以纯 Python 对象逐层走序列化路径,从而获得显著更好的性能。

关于这一点,官方教程 response-model.md 的原文说明是:使用 Pydantic(基于 Rust 实现核心)序列化 JSON 会 much faster(快得多)。也就是说,"性能优化"这个配方与配方一其实是同一套机制的两个收益面——声明返回类型/响应模型,既拿到安全过滤,也拿到高效 JSON 序列化。因此:

  • 凡是能声明响应模型的地方,优先声明,避免返回裸的 dict / Python 对象让框架逐层手工转换;
  • 同时获得 OpenAPI 文档中的响应 JSON Schema 与客户端代码生成支持。

需要补充的是,FastAPI 自身的响应序列化路径并不止一处用到编码器:除了上面提到的 fastapi/routing.py 常规响应编码,流式场景里也会用到(如 fastapi/routing.pyitem.data 的 JSON 编码)。也就是说,"声明返回类型 → 交给 Pydantic/Rust 做序列化"是贯穿常规响应与部分流式响应的统一思路。


配方三:用 tags 给 API 文档分类

当你积累了大量接口后,最自然的文档组织方式是给相关 path operation 打上同一个标签(tag),让自动文档 UI(Swagger UI / ReDoc)把它们归组显示。做法是给装饰器传入 tags 参数,值为 str 的列表(通常只放一个字符串):

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: set[str] = set()


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


@app.get("/items/", tags=["items"])
async def read_items():
    return [{"name": "Foo", "price": 42}]


@app.get("/users/", tags=["users"])
async def read_users():
    return [{"username": "johndoe"}]

(完整示例见 docs_src/path_operation_configuration/tutorial002_py310.py。)

标签会进入 OpenAPI schema,并被自动文档界面用来分组展示。

大型项目建议用 Enum 固化标签名,避免拼写漂移导致同类接口被分到不同标签下:

from enum import Enum

from fastapi import FastAPI

app = FastAPI()


class Tags(Enum):
    items = "items"
    users = "users"


@app.get("/items/", tags=[Tags.items])
async def get_items():
    return ["Portal gun", "Plumbus"]


@app.get("/users/", tags=[Tags.users])
async def read_users():
    return ["Rick", "Morty"]

(完整示例见 docs_src/path_operation_configuration/tutorial002b_py310.py。)

详细教程与分组效果见 Path Operation Configuration - Tags。若还要给每个标签写描述、外部文档等额外元数据,见本文"配方八"的 openapi_tags 部分。


配方四:为接口补充 summary 与 description

自动文档默认只会展示接口的路径与请求/响应模型,读者往往难以理解接口的业务含义。可以通过装饰器参数补齐说明:

@app.post(
    "/items/",
    summary="Create an item",
    description="Create an item with all the information, name, description, price, tax and a set of unique tags",
)
async def create_item(item: Item) -> Item:
    return item

(完整示例见 docs_src/path_operation_configuration/tutorial003_py310.py。)

对于较长的多行说明,官方推荐把描述写进函数的 docstring,FastAPI 会自动读取并支持 Markdown 渲染(会正确处理 docstring 缩进)。例如 docs_src/path_operation_configuration/tutorial005_py310.py 的 docstring 写法:

@app.post(
    "/items/",
    summary="Create an item",
    response_description="The created item",
)
async def create_item(item: Item) -> Item:
    """
    Create an item with all the information:

    - **name**: each item must have a name
    - **description**: a long description
    - **price**: required
    - **tax**: if the item doesn't have tax, you can omit this
    - **tags**: a set of unique tag strings for this item
    """
    return item

详细说明见 Path Operation Configuration - Summary and description


配方五:单独描述"响应"本身(response_description)

description 描述的是整个 path operation,而响应自身也可以有独立说明。装饰器参数 response_description 专门用于描述响应,它会在文档 UI 中展示在对应响应上:

@app.post(
    "/items/",
    summary="Create an item",
    response_description="The created item",
)
async def create_item(item: Item) -> Item:
    return item

注意:OpenAPI 规范要求每个 path operation 都有响应描述,因此即使你不传 response_description,FastAPI 也会自动生成一句 "Successful response"。详见 Path Operation Configuration - Response description


配方六:标记已废弃(deprecated)但暂不删除的接口

当你需要对某个接口标记为"过时、不推荐使用",又不想立即删除它时,在装饰器传入 deprecated=True。文档 UI 中会以明确的样式标示废弃状态,同时仍可访问:

@app.get("/items/", tags=["items"])
async def read_items():
    return [{"name": "Foo", "price": 42}]


@app.get("/users/", tags=["users"])
async def read_users():
    return [{"username": "johndoe"}]


@app.get("/elements/", tags=["items"], deprecated=True)
async def read_elements():
    return [{"item_id": "Foo"}]

(完整示例见 docs_src/path_operation_configuration/tutorial006_py310.py。)

详见 Path Operation Configuration - Deprecate a path operation


配方七:把任意数据转成 JSON 兼容结构(jsonable_encoder)

很多场景要求数据必须是"JSON 兼容"的:例如把数据写入只接受 JSON 兼容格式的数据库;datetime 对象在 JSON 里并不存在,必须转成 ISO 8601 字符串;Pydantic 模型是带属性的对象,存储时往往要转成普通 dict。FastAPI 为此提供了 jsonable_encoder() 工具函数。

看官方示例——一个"数据库只收 JSON 兼容数据"的场景:

from datetime import datetime

from fastapi import FastAPI
from fastapi.encoders import jsonable_encoder
from pydantic import BaseModel

fake_db = {}


class Item(BaseModel):
    title: str
    timestamp: datetime
    description: str | None = None


app = FastAPI()


@app.put("/items/{id}")
def update_item(id: str, item: Item):
    json_compatible_item_data = jsonable_encoder(item)
    fake_db[id] = json_compatible_item_data

(完整示例见 docs_src/encoder/tutorial001_py310.py。)

这里 jsonable_encoder(item) 会:把 Pydantic 模型转成 dict,把 datetime 转成 ISO 格式字符串……总之返回的是一棵所有值都 JSON 兼容的标准 Python 数据结构(如 dict/list),可以直接交给标准库 json.dumps(),但它本身不是一大段 JSON 字符串。

从源码看,该函数定义于 fastapi/encoders.py,签名参数与 Pydantic 的序列化控制一一对应:

参数 作用
include / exclude 控制要包含/排除的字段(传给 Pydantic 模型)
by_alias 是否按别名输出字段名(默认 True——API 中设置了 alias 多半就是想在结果中使用它)
exclude_unset 是否排除"未显式设置、仅取默认值"的字段
exclude_defaults 是否排除"值与默认值相同"的字段(即使显式设置过)
exclude_none 是否排除值为 None 的字段
custom_encoder 自定义编码器字典

值得强调的是,这个函数并不是只在用户手里才有用——FastAPI 内部就是通过它来把响应内容转换成 JSON 兼容数据后再交给 JSON 响应类输出的(见 fastapi/routing.pyreturn jsonable_encoder(response_content) 的调用点)。仓库对应的回归测试见 tests/test_jsonable_encoder.py。教程原文见 JSON Compatible Encoder


配方八:OpenAPI 应用级元数据(title / description / version / contact / license 等)

通用配方页提到的另一类高频问题是如何为整个 OpenAPI schema 添加元数据。这些元数据由 FastAPI() 构造参数提供,会进入 OpenAPI 规范并被文档 UI 展示。核心参数如下:

参数 类型 说明
title str API 标题
summary str API 简短摘要(自 OpenAPI 3.1.0 / FastAPI 0.99.0 起可用,据官方文档)
description str API 描述,支持 Markdown
version str 你自己应用的版本(不是 OpenAPI 的版本),例如 2.5.0
terms_of_service str 服务条款 URL
contact dict 联系方式,可含 nameurlemail
license_info dict 许可证信息,可含 name(必填)、url;OpenAPI 3.1.0+ 还支持 identifier(SPDX 表达式,与 url 二选一)

一个带全量元数据的构造示例:

from fastapi import FastAPI

description = """
ChimichangApp API helps you do awesome stuff.

## Items

You can **read items**.

## Users

You will be able to:

* **Create users** (_not implemented_).
* **Read users** (_not implemented_).
"""

app = FastAPI(
    title="ChimichangApp",
    description=description,
    version="0.0.1",
    terms_of_service="https://example.com/terms/",   # 替换为你的真实地址
    contact={
        "name": "Deadpoolio the Amazing",
        "url": "https://example.com/contact/",        # 替换为你的真实地址
        "email": "dp@example.com",                    # 替换为你的真实邮箱
    },
    license_info={
        "name": "Apache 2.0",
        "url": "https://www.apache.org/licenses/LICENSE-2.0.html",
    },
)

(完整示例见 docs_src/metadata/tutorial001_py310.pydescription 中支持 Markdown,渲染时按富文本展示。另一个使用 SPDX identifier 的变体见 docs_src/metadata/tutorial001_1_py310.py。)

为 tags 补充元数据(openapi_tags:为了让标签在文档中不只是名字,还可以给 FastAPI()openapi_tags——一个字典列表,每个字典描述一个标签:

  • name(必填):与 path operation / APIRoutertags 参数一致的标签名;
  • description:标签说明,支持 Markdown,显示在文档 UI 中;
  • externalDocs:外部文档说明,内含 descriptionurl(必填)。
tags_metadata = [
    {
        "name": "users",
        "description": "Operations with users. The **login** logic is also here.",
    },
    {
        "name": "items",
        "description": "Manage items. So _fancy_ they have their own docs.",
        "externalDocs": {
            "description": "Items external docs",
            "url": "https://example.com/items-docs/",   # 替换为你的真实地址
        },
    },
]

app = FastAPI(openapi_tags=tags_metadata)

(完整示例见 docs_src/metadata/tutorial004_py310.py。注意描述中的 Markdown 会被渲染,如 **login** 显示为加粗、_fancy_ 显示为斜体。)

两个使用细节:

  • 不需要为每个用到的 tag 都写元数据,缺省也无妨;
  • openapi_tags 列表的排列顺序就是文档 UI 中标签的展示顺序——例如 users 即使按字母序排在 items 之后,只要把它的字典放在列表首位,文档里就会先显示它。

详见 Metadata and Docs URLs


配方九:自定义(或彻底移除)OpenAPI schema 的 URL

默认情况下 OpenAPI schema 在 /openapi.json 提供。通过 FastAPI(openapi_url=...) 可以改到任意路径,例如放到 /api/v1/openapi.json

app = FastAPI(openapi_url="/api/v1/openapi.json")

(完整示例见 docs_src/metadata/tutorial002_py310.py。)

openapi_url=None 可以彻底禁用 OpenAPI schema——注意这会同时禁用依赖它的两个文档界面。详见 Metadata and Docs URLs - OpenAPI URL


配方十:自定义两个自动文档 UI 的 URL

FastAPI 内置两套自动文档界面,均可通过构造参数定制甚至关闭:

文档 UI 默认路径 定制参数 关闭方式
Swagger UI /docs docs_url docs_url=None
ReDoc /redoc redoc_url redoc_url=None

例如把 Swagger UI 放到 /documentation,同时关掉 ReDoc:

app = FastAPI(docs_url="/documentation", redoc_url=None)

(完整示例见 docs_src/metadata/tutorial003_py310.py。)

详见 Metadata and Docs URLs - Docs URLs


把配方串起来:一整套"可文档化、可过滤、可高效编码"的实践清单

结合上面十个配方,一个规范的 FastAPI 接口应当同时满足:

  1. 接口层:用 tags(字符串或 Enum)归类,用 summary/description(或 docstring 里的 Markdown)说清用途,用 response_description 说明响应,必要时用 deprecated=True 保留但不推荐旧接口(见 path-operation-configuration 示例集);
  2. 数据层:输入模型与输出模型分离(或继承同一基类),用返回类型注解 / response_model 让 FastAPI 做校验、OpenAPI 文档化、Rust 侧 JSON 序列化与安全过滤(见 response_model 示例集);
  3. 存储/中间层:落库或对接只接受 JSON 兼容数据的系统前,用 jsonable_encoder()(位置见 fastapi/encoders.py)统一转换,控制 include/exclude/by_alias/exclude_unset/exclude_defaults/exclude_none 等细粒度输出(相关测试见 tests/test_jsonable_encoder.py);
  4. 应用层:在 FastAPI() 上配置 title/description/version/contact/license_info 等元数据与 openapi_tags 标签说明,按需定制 openapi_urldocs_urlredoc_url,控制 OpenAPI 与文档界面的暴露路径(见 metadata 示例集)。

以上配方的完整英文源文档分别位于 response-model.mdpath-operation-configuration.mdencoder.mdmetadata.md;所有中文/印地语等多语言翻译版本在 docs 目录下按语言组织(关联的印地语导航页即 docs/hi/docs/how-to/general.md)。对照示例代码(docs_src)逐一运行,即可把这些"通用配方"直接落到你自己的项目里。

登录后查看全文
热门项目推荐
相关项目推荐