Skip to content

Commit 8c677f5

Browse files
committed
add post: fastapi response model
1 parent d94fa43 commit 8c677f5

File tree

6 files changed

+404
-4
lines changed

6 files changed

+404
-4
lines changed
Lines changed: 324 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,324 @@
1+
+++
2+
date = '2025-08-12T10:00:00+08:00'
3+
draft = true
4+
title = 'FastAPI Response Model'
5+
+++
6+
本篇文章介绍 FastAPI 的返回类型 response model
7+
8+
可以在返回函数的类型注解中声明该接口的响应数据类型
9+
10+
类型注解的用法和输入数据参数一样, 可以使用:
11+
- Pydantic 模型
12+
- list 列表
13+
- dict 字典
14+
- scalar 标量值 (int, bool ...)
15+
16+
```Python
17+
@app.post("/items/")
18+
async def create_item(item: Item) -> Item:
19+
...
20+
21+
@app.get("/items/")
22+
async def read_items() -> list[Item]:
23+
...
24+
```
25+
26+
FastAPI 会使用返回类型完成一下事情:
27+
- 验证返回类型
28+
如果返回的数据无效, 说明业务代码有问题, FastAPI 会返回服务器错误, 而不是把数据发给客户端
29+
30+
- 在 OpenAPI 中为响应添加 JSON Schema
31+
用于自动生成接口文档, 自动生成客户端代码
32+
33+
- 最重要的是
34+
它会限制并过滤出数据, 只保留返回类型中定义的字段
35+
36+
37+
### `response_model` Parameter
38+
有时候可能需要返回的数据和类型注解不完全一致, 例如:
39+
- 可能想返回字典或数据库对象, 但声明的响应类型为 Pydantic 模型
40+
- 这样 Pydantic 会做数据文档、验证等工作, 即使返回的是字典或 ORM 对象
41+
42+
如果直接用返回类型注解, 编辑器会提示类型不匹配的错误
43+
44+
这种情况下, 可以用路径装饰器的 `response_model` 参数来声明响应类型, 而不是用返回类型注解
45+
46+
```Python
47+
class Item(BaseModel):
48+
name: str
49+
description: str | None = None
50+
price: float
51+
tax: float | None = None
52+
tags: list[str] = []
53+
54+
@app.post("/items/", response_model=Item)
55+
async def create_item(item: Item) -> Any:
56+
return item
57+
58+
@app.get("/items/", response_model=list[Item])
59+
async def read_items() -> Any:
60+
return [
61+
{"name": "Portal Gun", "price": 42.0},
62+
{"name": "Plumbus", "price": 32.0},
63+
]
64+
```
65+
66+
注意:
67+
- `response_model` 是装饰器(`get``post` 等方法)的参数, 不是函数的参数
68+
- 接收的类型和 Pydantic 字段定义一样, 可以是单个模型, 也可以是模型列表等
69+
- FastAPI 用其做数据库验证、文档生成、以及过滤输出数据
70+
71+
> 如果使用 mypy 之类做 static type check, 可以声明函数返回类型为 `Any`
72+
73+
- `response_model` 优先级
74+
如果同时声明了 `response_model` 和返回类型, 则 `response_model` 会优先生效
75+
如果想要禁用响应模型, 可以设置 `response_model=None` (用于一些非 Pydantic 类型的返回值)
76+
77+
78+
### Return the Same Input Data 返回相同数据数据
79+
很多情况下, 希望模型返回与输入模型相同的数据
80+
81+
这式, 可以在路径函数中直接声明 `response_model=YourModel`, FastAPI 会自动处理
82+
83+
```Python
84+
from fastapi import FastAPI
85+
from pydantic import BaseModel, EmailStr
86+
87+
app = FastAPI()
88+
89+
class UserIn(BaseModel):
90+
username: str
91+
password: str
92+
email: EmailStr
93+
full_name: str | None = None
94+
95+
# Don't do this in production!
96+
@app.post("/user/")
97+
async def create_user(user: UserIn) -> UserIn:
98+
return user
99+
```
100+
> 不要在生产环境中以明文形式存储用户密码, 也不要像这样直接返回密码
101+
102+
### Add an Output Model 添加输出模型
103+
我们可以改成: 输入模型包含明文密码, 输出模型不含
104+
```Python
105+
from typing import Any
106+
107+
from fastapi import FastAPI
108+
from pydantic import BaseModel, EmailStr
109+
110+
app = FastAPI()
111+
112+
# Input model
113+
class UserIn(BaseModel):
114+
username: str
115+
password: str
116+
email: EmailStr
117+
full_name: str | None = None
118+
119+
# Output model
120+
class UserOut(BaseModel):
121+
username: str
122+
email: EmailStr
123+
full_name: str | None = None
124+
125+
126+
@app.post("/user/", response_model=UserOut) # output
127+
async def create_user(user: UserIn) -> Any:
128+
return user # like input
129+
```
130+
这样, 即使路径操作函数返回的对象中包含该字段, FastAPI 也会按照 `response_model=UserOut` 来过滤密码
131+
132+
133+
### Return Type and Data Filtering 返回类型与数据过滤
134+
延续上面的例子, 希望函数的类型注解和实际返回值不同:
135+
136+
函数返回的对象可能包含更多数据, 但响应中只保留输出模型声明的字段
137+
138+
之前由于类不同, 只能用 `response_model`, 这样就失去了编辑器和类型检查对返回值的检查
139+
140+
大多数情况下, 我们只是想去掉或过滤掉部分数据, 这时可以用 类继承(classes and inheritance) 来兼顾类型注解和数据过滤
141+
```Python
142+
class BaseUser(BaseModel):
143+
username: str
144+
email: EmailStr
145+
full_name: str | None = None
146+
147+
class UserIn(BaseUser):
148+
password: str
149+
150+
@app.post("/user/")
151+
async def create_user(user: UserIn) -> BaseUser:
152+
return user
153+
```
154+
通过这种方式:
155+
1. Type Annotations and Testing 编辑器和类型检查工具支持: `UserIn``BaseUser` 的子类, 返回 `UserIn` 实例完全符合 `BaseUser` 类型要求
156+
2. FastAPI Data Filtering 数据过滤: 响应中会自动去掉 `password` 字段, 只保留 `BaseUser` 中声明的字段
157+
158+
159+
### Other Return Type Annotations 其他类型注解
160+
有些时候, 返回的内容不是有效的 Pydantic 字段, 但在函数中添加了注解, 为了获取工具支持
161+
162+
#### Return a response directly 直接返回响应
163+
最常见的就是直接返回一个 Resposne
164+
```Python
165+
from fastapi import FastAPI, Response
166+
from fastapi.resposnes import JSONResponse, RedirectResponse
167+
168+
app = FastAPI()
169+
170+
@app.get("/portal")
171+
async def get_protal(teleport: bool = False) -> Response:
172+
if teleport:
173+
return RedirectResponse(url="https://www.youtube.com/watch?v=dQw4w9WgXcQ")
174+
return JSONResponse(content={"message": "Here's your interdimensional portal."})
175+
```
176+
这种简单情况由 FastAPI 自动处理, 因为返回类型注解是 Response 类
177+
178+
开发工具也能正常工作, 因为 `RedirectResponse``JSONResponse` 都是 `Response` 的子类, 所以类型注解是正确的
179+
180+
181+
#### Invalid return type annotations 无效的类型注解
182+
但是, 当返回一些其他任意对象(不是有效的 Pydantic 类型, 例如数据库对象)并在函数中这样注解时, FastAPI 会尝试从该类型注解创建一个 Pydantic 响应模型, 然后会失败
183+
184+
如果使用了联合模型, 其中有一个或多个不是有效的 Pydantic 类型, 同样会失败
185+
```Python
186+
from fastapi import FastAPI, Response
187+
from fastapi.responses import RedirectResponse
188+
189+
app = FastAPI()
190+
191+
@app.get("/portal")
192+
async def get_portal(teleport: bool = False) -> Response | dict:
193+
if teleport:
194+
return RedirectResponse(url="https://www.youtube.com/watch?v=dQw4w9WgXcQ")
195+
return {"message": "Here's your interdimensional portal."}
196+
```
197+
这会失败是因为类型注解不是单一的 Pydantic 类型, 也不是单一的 Response 类或子类, 而是 Response 和 dict 之间的联合类型
198+
199+
#### Disable response Model 禁用响应类型
200+
如果不希望 FastAPI 执行默认的数据验证、文档生成、过滤等操作, 但是又想在函数中保留返回类型注解, 以获得编辑器和类型检查工具的支持, 这种情况下设置 `response_model=None` 来禁用响应生成
201+
```Python
202+
from fastapi import FastAPI, Response
203+
from fastapi.response import RedirectResponse
204+
205+
app = FastAPI()
206+
207+
@app.get("/portal", response_model=None)
208+
async def get_protal(teleport: bool = False) -> Response | dict:
209+
if teleport:
210+
return RedirectResponse(url="https://www.youtube.com/watch?v=dQw4w9WgXcQ")
211+
return JSONResponse(content={"message": "Here's your interdimensional portal."})
212+
```
213+
214+
215+
### Response Model Encoding Parameters 响应模型编码参数
216+
响应模型可能有默认值
217+
```Python
218+
class Item(BaseModel):
219+
name: str
220+
description: str | None = None
221+
price: float
222+
tax: float = 10.5
223+
tags: list[str] = []
224+
```
225+
例如, 在 NoSQL 数据库中哟许多可选属性的模型, 但不想发送默认值的很长的 JSON 响应
226+
227+
可以使用 path operation operator 的 `response_model_exclude_mode` 参数来去除默认值
228+
```Python
229+
@app.get("/items/{item_id}", response_model=Item, response_model_exclude_unset=True)
230+
async def read_item(item_id: str):
231+
return items[item_id]
232+
```
233+
- `description: str | None = None` 默认值为 `None`
234+
- `tax: float = 10.5` 的默认值为 10.5
235+
- `tags: List[str] = []` 的默认值是空列表 `[]`
236+
237+
此时, 如果向该路径发送 ID 为 `foo` 的项目请求
238+
```yaml
239+
"foo": {"name": "Foo", "price": 50.2}
240+
```
241+
242+
响应将是
243+
```yaml
244+
{
245+
"name": "foo",
246+
"price": 50.2,
247+
}
248+
```
249+
250+
- `response_model_include``response_model_exclude`
251+
252+
也可以使用 path operation parameter 中的 `response_model_include``response_model_exclude`, 他们接受一个包含属性名称字符串的 `set`, 用于包含(省略其余部分)或排除(包含其余部分)
253+
254+
```Python
255+
from fastapi import FastAPI
256+
from pydantic import BaseModel
257+
258+
app = FastAPI()
259+
260+
class Item(BaseModel):
261+
name: str
262+
description: str | None = None
263+
price: float
264+
tax: float = 10.5
265+
266+
267+
items = {
268+
"foo": {"name": "Foo", "price": 50.2},
269+
"bar": {"name": "Bar", "description": "The Bar fighters", "price": 62, "tax": 20.2},
270+
"baz": {
271+
"name": "Baz",
272+
"description": "There goes my baz",
273+
"price": 50.2,
274+
"tax": 10.5,
275+
},
276+
}
277+
278+
279+
@app.get(
280+
"/items/{item_id}/name",
281+
response_model=Item,
282+
response_model_include={"name", "description"}, # 包含
283+
)
284+
async def read_item_name(item_id: str):
285+
return items[item_id]
286+
287+
288+
@app.get(
289+
"/items/{item_id}/public",
290+
response_model=Item,
291+
response_model_exclude={"tax"}, # 排除
292+
)
293+
async def read_item_public_data(item_id: str):
294+
return items[item_id]
295+
```
296+
297+
虽然可以通过上述方法自定义返回参数包含哪些, 但还是建议使用多个类来实现该功能, 而不这些参数
298+
299+
这是因为, 即使使用 `response_model_include``response_model_exclude` 来省略某些属性, 在应用程序的 OpenAPI 中生成的 JSON Schema 仍将是完整模型的 Schema
300+
301+
这对于 `response_model_by_alias` 也是一样的
302+
303+
#### Using `list` instead of `set`s
304+
如果忘记使用集和而使用元组或列表, FastAPI 仍会将其转换为集和, 确保正常工作
305+
```Python
306+
@app.get(
307+
"/items/{item_id}/name",
308+
response_model=Item,
309+
response_model_include=["name", "description"], # 使用列表
310+
)
311+
async def read_item_name(item_id: str):
312+
return items[item_id]
313+
314+
@app.get(
315+
"/items/{item_id}/public",
316+
response_model=Item,
317+
response_model_exclude=["tax"], # 使用列表
318+
)
319+
async def read_item_public_data(item_id: str):
320+
return items[item_id]
321+
```
322+
323+
324+

0 commit comments

Comments
 (0)