FastAPI Response Model 响应模型全解析用返回类型与response_model精准定义、校验并过滤接口输出【免费下载链接】fastapiFastAPI framework, high performance, easy to learn, fast to code, ready for production项目地址: https://gitcode.com/GitHub_Trending/fa/fastapi本篇文章基于 FastAPI 官方教程Response Model - Return Type源码示例位于 docs_src/response_model展开。它系统讲解如何通过path operation function 的返回类型注解或装饰器参数response_model来声明响应结构以及 FastAPI 如何基于此完成响应数据校验、OpenAPI JSON Schema 生成、JSON 序列化与最关键的输出字段过滤。读完本文你将掌握在真实业务中为每个接口削平多余数据尤其防止密码等隐私字段外泄的完整方案并理解返回类型、response_model与各类响应编码参数response_model_exclude_unset、response_model_include等的取舍与底层原理。一、返回类型即响应契约用 Return Type 声明响应结构FastAPI 允许你像给函数参数声明输入类型那样用path operation function的返回类型return type来声明响应的数据类型。返回类型可以是 Pydantic model、list、dict、整数/布尔等标量值也可以是它们的任意合法组合。最基础的写法如下完整示例见 tutorial001_01_py310.pyfrom 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(namePortal Gun, price42.0), Item(namePlumbus, price32.0), ]其中第 16 行- Item与第 21 行- list[Item]就是响应声明。FastAPI 会把这个返回类型用于四件事校验Validate返回数据如果函数实际返回的数据不合法例如缺少某个必填字段说明你的应用代码出了问题——它没有返回它本该返回的东西。此时 FastAPI 会返回一个server error而不是带着错误数据继续响应从而让你和客户端都能确信收到的数据与数据结构是符合预期的。为 OpenAPI path operation 生成响应的 JSON Schema该 Schema 会被自动交互文档/docs使用也会被自动化的客户端代码生成工具使用。用 Pydantic 将返回数据序列化为 JSONPydantic 的核心序列化层由 Rust 编写因此这一过程非常快。最关键的是把输出数据限制并过滤到返回类型所定义的范围内。函数哪怕多返回了字段最终响应里也只保留类型中声明过的字段。这一点对安全尤其重要下文会反复看到它的价值。二、response_model参数当实际返回与类型声明不一致时2.1 为什么需要它有些场景下你实际返回的数据与类型注解所声明的并不完全吻合。例如你希望返回一个dict或数据库对象但想让 FastAPI 以某个 Pydantic model 的视角去完成数据文档化、校验等全部工作。此时如果直接写返回类型注解编辑器和静态检查工具会正确地报错你的函数返回了dict却声明返回 Pydantic model。解决办法是改用path operation decorator参数response_model。2.2 用法与作用范围response_model可以用在任意 path operation 上app.get()、app.post()、app.put()、app.delete()等。示例见 tutorial001_py310.pyfrom 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_modelItem) async def create_item(item: Item) - Any: return item app.get(/items/, response_modellist[Item]) async def read_items() - Any: return [ {name: Portal Gun, price: 42.0}, {name: Plumbus, price: 32.0}, ]这里函数实际返回的是裸dict与dict列表但通过response_model声明了输出契约。需要注意response_model是装饰器方法get、post等的参数不是path operation function自身的参数函数参数是路径参数、查询参数和请求体那一套。response_model接收的类型与你在 Pydantic model 字段里声明的类型相同所以它可以是一个 Pydantic model也可以是如List[Item]这样的 Pydantic model 列表。FastAPI 会用该response_model完成数据文档化、校验等并把输出数据转换并过滤到其类型声明的范围。如果编辑器 / mypy 开启了严格类型检查你可以把函数返回类型声明为Any明确告诉编辑器我是有意返回任意内容的FastAPI 依然会依据response_model做文档化、校验与过滤。2.3response_model的优先级若同时声明了返回类型与response_modelresponse_model优先FastAPI 会使用它。这带来一个很好的实践即使实际返回类型与响应模型不同你也尽可以在函数上写正确的类型注解供编辑器与 mypy 使用同时仍让 FastAPI 通过response_model完成数据校验与文档化。另外可用response_modelNone关闭该 path operation 的响应模型生成。当你只是为某些并非合法 Pydantic 字段的东西添加类型注解时后文会有示例就必须这样做。从实现上看这些参数最终都会落入路由的序列化流程。在 fastapi/routing.py 中APIRoute保存了response_field、response_model_include、response_model_exclude、response_model_by_alias、response_model_exclude_unset、response_model_exclude_defaults、response_model_exclude_none等成员序列化响应时参见 fastapi/routing.py 与 fastapi/routing.py会把include、exclude、by_alias、exclude_unset、exclude_defaults、exclude_none一起交给 Pydantic 完成数据过滤与输出。三、经典陷阱把输入模型直接当输出模型明文密码回显下面定义一个含明文密码的输入模型UserIn示例见 tutorial002_py310.pyfrom fastapi import FastAPI from pydantic import BaseModel, EmailStr app FastAPI() class UserIn(BaseModel): username: str password: str email: EmailStr full_name: str | None None # Dont do this in production! app.post(/user/) async def create_user(user: UserIn) - UserIn: return user使用EmailStr前需要先安装校验器依赖email-validator。可用以下任一命令加入项目$ uv add email-validator或用$ uv add pydantic[email]本例用同一个UserIn既声明输入又声明输出。当浏览器带着密码创建用户时API 会在响应里原样返回这个密码。对创建者本人也许不算泄露可一旦把同一个模型复用到其它 path operation就可能把用户的密码发给每一个客户端。⚠️危险除非你完全清楚其中的各种陷阱并且知道自己正在做什么否则永远不要以这种方式存储或返回用户的明文密码。四、正确做法分离输入/输出模型由 FastAPI 过滤隐私字段更稳妥的方案是建两个模型带明文密码的输入模型UserIn与不含密码的输出模型UserOut。完整代码见 tutorial003_py310.pyfrom 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_modelUserOut) async def create_user(user: UserIn) - Any: return user这里虽然path operation function返回的仍是含密码的输入用户对象但response_modelUserOut声明了不含password的输出契约。因此FastAPI 会负责借助 Pydantic把输出模型未声明的所有数据过滤掉客户端永远拿不到密码字段。4.1response_model还是返回类型本案例中UserIn与UserOut是两个不同的类。若把函数返回类型注解成UserOut编辑器与工具会立刻报返回了无效类型——因为函数实际返回的是另一个类的实例。这正是本例必须使用response_model参数的原因。五、返回类型与数据过滤用类继承兼得工具支持与字段裁剪上一节为了过滤数据不得不放弃返回类型带来的工具支持。但在绝大多数只想从返回结果里裁掉一些字段的场景中可以借助类的继承同时获得两者。先看官方给出的升级版写法完整代码见 tutorial003_01_py310.pyfrom 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 userBaseUser持有基础字段UserIn(BaseUser)继承BaseUser并追加password字段因而包含全部字段函数返回类型注解为BaseUser实际返回的却是UserIn实例。这样写之后编辑器、mypy 等工具不抱怨UserIn是BaseUser的子类类型上合法FastAPI 又依据BaseUser对输出做了过滤。这是怎么做到的5.1 工具视角类型注解为什么合法从类型系统看UserIn是BaseUser的子类凡期望任意BaseUser的位置UserIn都是合法类型所以编辑器与 mypy 不会报错代码补全与类型检查能力得以保留。5.2 FastAPI 视角过滤时不采用继承规则对于 FastAPI它会读取返回类型并确保你返回的数据只包含类型里声明的字段。关键点在于FastAPI 在内部借助 Pydantic 做了若干处理避免类的继承规则被套用到返回数据的过滤上——否则子类新增的字段会随继承一起被返回最终吐出的数据将远超预期。这一行为的正确性在仓库测试中得到专门验证例如 tests/test_response_model_data_filter.py 与 tests/test_response_model_data_filter_no_inheritance.py 覆盖了返回类型为父类、实际返回子类/含额外字段对象时字段应被裁剪的语义。这样你就能同时拿到两样好处有工具支持的返回类型注解FastAPI 的数据过滤。六、在自动文档中验证效果打开自动交互文档可以确认输入模型与输出模型各自拥有独立的 JSON Schema两个模型也分别被用于交互式 API 文档请求体Request body按含password的UserIn渲染200 响应示例则按不含password的UserOut渲染七、其它返回类型注解直接返回 Response 的场景有时你会返回一些并非合法 Pydantic 字段的对象却仍想在函数上写返回类型注解目的只是获取编辑器与 mypy 的工具支持。下面是几类典型情况。7.1 直接返回Response最常见的场景是像高级教程里讲的那样直接返回一个Response。示例见 tutorial003_02_py310.pyfrom 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(urlhttps://www.youtube.com/watch?vdQw4w9WgXcQ) return JSONResponse(content{message: Heres your interdimensional portal.})当返回类型注解是Response类或其子类时FastAPI 会自动处理这个简单情况不会尝试把它当作 Pydantic model。同时工具也满意——RedirectResponse、JSONResponse都是Response的子类注解类型是准确的。7.2 注解一个Response子类你还可以在注解中直接使用Response的子类。见 tutorial003_03_py310.pyfrom fastapi import FastAPI from fastapi.responses import RedirectResponse app FastAPI() app.get(/teleport) async def get_teleport() - RedirectResponse: return RedirectResponse(urlhttps://www.youtube.com/watch?vdQw4w9WgXcQ)这同样成立RedirectResponse是Response的子类FastAPI 会自动处理该简单情况。7.3 非法的返回类型注解会报错 但若返回的是数据库对象等任意非 Pydantic 类型对象并把它写成返回类型注解FastAPI 会尝试从这个注解创建 Pydantic response model 并失败。同理若在多种类型之间使用union联合类型这些类型中的任意一个而其中一种或多种并非合法 Pydantic 类型也会失败。例如 tutorial003_04_py310.pyfrom 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(urlhttps://www.youtube.com/watch?vdQw4w9WgXcQ) return {message: Heres your interdimensional portal.}它失败的原因在于该类型注解既不是合法 Pydantic 类型也不是单一的Response类或其子类而是Response与dict的联合二选一。7.4 关闭响应模型response_modelNone承接上面的例子你可能既不想要 FastAPI 默认的数据校验、文档化与过滤又想在函数上保留返回类型注解以获取编辑器与类型检查器如 mypy的支持。此时设置response_modelNone即可。见 tutorial003_05_py310.pyfrom fastapi import FastAPI, Response from fastapi.responses import RedirectResponse app FastAPI() app.get(/portal, response_modelNone) async def get_portal(teleport: bool False) - Response | dict: if teleport: return RedirectResponse(urlhttps://www.youtube.com/watch?vdQw4w9WgXcQ) return {message: Heres your interdimensional portal.}response_modelNone会让 FastAPI 跳过该 path operation 的响应模型生成于是你可以随意书写需要的返回类型注解而不会影响 FastAPI 应用本身。八、响应编码参数控制默认值是否进入响应先看一个带默认值的响应模型完整代码见 tutorial004_py310.pyfrom 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_modelItem, response_model_exclude_unsetTrue) async def read_item(item_id: str): return items[item_id]其中的默认值包括description: str | None NonePython 3.10 中Union[str, None] None的简写默认为Nonetax: float 10.5默认为10.5tags: list[str] []默认为空列表[]。如果这些值在数据源中其实并未被存储你可能不希望它们混进响应。典型的例子是 NoSQL 数据库里带大量可选属性的模型——你不想发送一份塞满默认值、又长又冗余的 JSON 响应。8.1response_model_exclude_unsetTrue在装饰器上设置response_model_exclude_unsetTrue后那些默认值不会出现在响应里只有真正被设置的字段才会输出。例如向 ID 为foo的条目发起请求该条目只存了name与price响应不含默认值将是{ name: Foo, price: 50.2 }提示还可以配合使用response_model_exclude_defaultsTrueresponse_model_exclude_noneTrue其语义分别对应 Pydantic 的exclude_defaults与exclude_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: [] }这里的description、tax、tags与默认值完全相同。FastAPI准确地说是 Pydantic足够聪明能分辨出这些字段是被显式设置的而非取自默认值因此它们依然会被包含在 JSON 响应里。提示默认值不一定是None可以是任意内容——空列表[]、浮点数10.5等都没问题。8.2response_model_include与response_model_exclude你还可以使用装饰器参数response_model_include与response_model_exclude。它们接收一个由属性名字符串构成的set前者表示只包含这些属性其余省略后者表示排除这些属性其余包含。当你只有一个 Pydantic model、又想从输出里删掉部分字段时这是快捷方式。示例见 tutorial005_py310.pyfrom 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_modelItem, response_model_include{name, description}, ) async def read_item_name(item_id: str): return items[item_id] app.get(/items/{item_id}/public, response_modelItem, response_model_exclude{tax}) async def read_item_public_data(item_id: str): return items[item_id]其中语法{name, description}会生成包含这两个值的set等价于set([name, description])。建议相比这些参数仍然推荐使用前文多类 继承的思路。原因在于即使你用response_model_include/response_model_exclude省略了某些属性OpenAPI及自动文档中生成的 JSON Schema依然是完整模型的那一份——这与你的实际输出并不完全对应。该提醒同样适用于行为类似的response_model_by_alias。用list代替set如果你忘了用set而写成list或tupleFastAPI 仍会自动把list/tuple转成set并正常工作。示例见 tutorial006_py310.py其路由定义如下app.get( /items/{item_id}/name, response_modelItem, response_model_include[name, description], ) async def read_item_name(item_id: str): return items[item_id] app.get(/items/{item_id}/public, response_modelItem, response_model_exclude[tax]) async def read_item_public_data(item_id: str): return items[item_id]九、小结Recap用path operation decorator的参数response_model定义响应模型尤其是确保隐私数据被过滤掉例如不要把输入模型里的密码等字段直接回显给客户端。当实际返回对象与输出结构不同不同类时优先通过response_model 多模型/类继承的组合来兼得类型工具支持与 FastAPI 的自动字段过滤。用response_model_exclude_unset等编码参数只返回那些被显式设置的字段从而避免响应被一堆默认值污染需要精确裁剪字段时再使用response_model_include/response_model_exclude并留意其对 OpenAPI Schema 完整性的影响。结合本仓库的实现参见 fastapi/routing.py 对相关参数的接收与 fastapi/routing.py、fastapi/routing.py 对 Pydantic 序列化的调用可以看出声明响应模型 → 校验 → 过滤 → 序列化是一条被框架集中处理的主链路。把输入与输出模型分开设计、把过滤交给框架而非手写是这套机制里最值得养成的习惯。【免费下载链接】fastapiFastAPI framework, high performance, easy to learn, fast to code, ready for production项目地址: https://gitcode.com/GitHub_Trending/fa/fastapi创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考