官网:https://fastapi.tiangolo.com/zh/tutorial/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},
]
Note
注意,
response_model
是「装饰器」方法(get
,post
等)的一个参数。不像之前的所有参数和请求体,它不属于路径操作函数。
它接收的类型与你将为 Pydantic 模型属性所声明的类型相同,因此它可以是一个 Pydantic 模型,但也可以是一个由 Pydantic 模型组成的 list
,例如 List[Item]
。
FastAPI 将使用此 response_model
来:
- 将输出数据转换为其声明的类型。
- 校验数据。
- 在 OpenAPI 的路径操作中为响应添加一个 JSON Schema。
- 并在自动生成文档系统中使用。
但最重要的是:
- 会将输出数据限制在该模型定义内。下面我们会看到这一点有多重要。
"技术细节"
响应模型在参数中被声明,而不是作为函数返回类型的注解,这是因为路径函数可能不会真正返回该响应模型,而是返回一个
dict
、数据库对象或其他模型,然后再使用response_model
来执行字段约束和序列化。
返回与输入相同的数据¶
现在我们声明一个 UserIn
模型,它将包含一个明文密码属性。
from typing import Union
from fastapi import FastAPI
from pydantic import BaseModel, EmailStr
app = FastAPI()
class UserIn(BaseModel):
username: str
password: str
email: EmailStr
full_name: Union[str, None] = None
# Don't do this in production!
@app.post("/user/")
async def create_user(user: UserIn) -> UserIn:
return user
我们正在使用此模型声明输入数据,并使用同一模型声明输出数据:
@app.post("/user/")
async def create_user(user: UserIn) -> UserIn:
return user
现在,每当浏览器使用一个密码创建用户时,API 都会在响应中返回相同的密码。
在这个案例中,这可能不算是问题,因为用户自己正在发送密码。
但是,如果我们在其他的路径操作中使用相同的模型,则可能会将用户的密码发送给每个客户端。
Danger
永远不要存储用户的明文密码,也不要在响应中发送密码。
添加输出模型¶
相反,我们可以创建一个有明文密码的输入模型和一个没有明文密码的输出模型:
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
这样,即便我们的路径操作函数将会返回包含密码的相同输入用户:
@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)。
在文档中查看¶
当你查看自动化文档时,你可以检查输入模型和输出模型是否都具有自己的 JSON Schema:
并且两种模型都将在交互式 API 文档中使用:
响应模型编码参数¶
你的响应模型可以具有默认值,例如:
from typing import List, Union
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel):
name: str
description: Union[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
具有默认值None
。tax: float = 10.5
具有默认值10.5
.tags: List[str] = []
具有一个空列表作为默认值:[]
.
但如果它们并没有存储实际的值,你可能想从结果中忽略它们的默认值。
举个例子,当你在 NoSQL 数据库中保存了具有许多可选属性的模型,但你又不想发送充满默认值的很长的 JSON 响应。
使用 response_model_exclude_unset
参数¶
你可以设置路径操作装饰器的 response_model_exclude_unset=True
参数:
@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 }
Info
FastAPI 通过 Pydantic 模型的
.dict()
配合 该方法的 exclude_unset 参数 来实现此功能。
Info
你还可以使用:
response_model_exclude_defaults=True
response_model_exclude_none=True
参考 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 响应中。
Tip
请注意默认值可以是任何值,而不仅是
None
。它们可以是一个列表(
[]
),一个值为10.5
的float
,等等。
response_model_include
和 response_model_exclude
¶
你还可以使用路径操作装饰器的 response_model_include
和 response_model_exclude
参数。
它们接收一个由属性名称 str
组成的 set
来包含(忽略其他的)或者排除(包含其他的)这些属性。
如果你只有一个 Pydantic 模型,并且想要从输出中移除一些数据,则可以使用这种快捷方法。
Tip
但是依然建议你使用上面提到的主意,使用多个类而不是这些参数。
这是因为即使使用
response_model_include
或response_model_exclude
来省略某些属性,在应用程序的 OpenAPI 定义(和文档)中生成的 JSON Schema 仍将是完整的模型。这也适用于作用类似的
response_model_by_alias
。
from typing import Union
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel):
name: str
description: Union[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]
Tip
{"name", "description"}
语法创建一个具有这两个值的set
。等同于
set(["name", "description"])
。
使用 list
而不是 set
¶
如果你忘记使用 set
而是使用 list
或 tuple
,FastAPI 仍会将其转换为 set
并且正常工作:
from typing import Union
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel):
name: str
description: Union[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
来仅返回显式设定的值。
更多模型¶
多个关联模型这种情况很常见。
特别是用户模型,因为:
- 输入模型应该含密码
- 输出模型不应含密码
- 数据库模型需要加密的密码
"危险"
千万不要存储用户的明文密码。始终存储可以进行验证的安全哈希值。
如果不了解这方面的知识,请参阅安全性中的章节,了解什么是密码哈希。
多个模型¶
下面的代码展示了不同模型处理密码字段的方式,及使用位置的大致思路:
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
class UserInDB(BaseModel):
username: str
hashed_password: str
email: EmailStr
full_name: str | None = None
def fake_password_hasher(raw_password: str):
return "supersecret" + raw_password
def fake_save_user(user_in: UserIn):
hashed_password = fake_password_hasher(user_in.password)
user_in_db = UserInDB(**user_in.dict(), hashed_password=hashed_password)
print("User saved! ..not really")
return user_in_db
@app.post("/user/", response_model=UserOut)
async def create_user(user_in: UserIn):
user_saved = fake_save_user(user_in)
return user_saved
**user_in.dict()
简介¶
Pydantic 的 .dict()
¶
user_in
是类 UserIn
的 Pydantic 模型。
Pydantic 模型支持 .dict()
方法,能返回包含模型数据的字典。
因此,如果使用如下方式创建 Pydantic 对象 user_in
:
user_in = UserIn(username="john", password="secret", email="[email protected]")
就能以如下方式调用:
user_dict = user_in.dict()
现在,变量 user_dict
中的就是包含数据的字典(变量 user_dict
是字典,不是 Pydantic 模型对象)。
以如下方式调用:
print(user_dict)
输出的就是 Python 字典:
{ 'username': 'john', 'password': 'secret', 'email': '[email protected]', 'full_name': None, }
解包 dict
¶
把字典 user_dict
以 **user_dict
形式传递给函数(或类),Python 会执行解包操作。它会把 user_dict
的键和值作为关键字参数直接传递。
因此,接着上面的 user_dict
继续编写如下代码:
UserInDB(**user_dict)
就会生成如下结果:
UserInDB(
username="john",
password="secret",
email="[email protected]",
full_name=None,
)
或更精准,直接把可能会用到的内容与 user_dict
一起使用:
UserInDB(
username = user_dict["username"],
password = user_dict["password"],
email = user_dict["email"],
full_name = user_dict["full_name"],
)
用其它模型中的内容生成 Pydantic 模型¶
上例中 ,从 user_in.dict()
中得到了 user_dict
,下面的代码:
user_dict = user_in.dict()
UserInDB(**user_dict)
等效于:
UserInDB(**user_in.dict())
……因为 user_in.dict()
是字典,在传递给 UserInDB
时,把 **
加在 user_in.dict()
前,可以让 Python 进行解包。
这样,就可以用其它 Pydantic 模型中的数据生成 Pydantic 模型。
解包 dict
和更多关键字¶
接下来,继续添加关键字参数 hashed_password=hashed_password
,例如:
UserInDB(**user_in.dict(), hashed_password=hashed_password)
……输出结果如下:
UserInDB(
username = user_dict["username"],
password = user_dict["password"],
email = user_dict["email"],
full_name = user_dict["full_name"],
hashed_password = hashed_password,
)
"警告"
辅助的附加函数只是为了演示可能的数据流,但它们显然不能提供任何真正的安全机制。
减少重复¶
FastAPI 的核心思想就是减少代码重复。
代码重复会导致 bug、安全问题、代码失步等问题(更新了某个位置的代码,但没有同步更新其它位置的代码)。
上面的这些模型共享了大量数据,拥有重复的属性名和类型。
FastAPI 可以做得更好。
声明 UserBase
模型作为其它模型的基类。然后,用该类衍生出继承其属性(类型声明、验证等)的子类。
所有数据转换、校验、文档等功能仍将正常运行。
这样,就可以仅声明模型之间的差异部分(具有明文的 password
、具有 hashed_password
以及不包括密码)。通过这种方式,可以只声明模型之间的区别(分别包含明文密码、哈希密码,以及无密码的模型)。
from fastapi import FastAPI
from pydantic import BaseModel, EmailStr
app = FastAPI()
class UserBase(BaseModel):
username: str
email: EmailStr
full_name: str | None = None
class UserIn(UserBase):
password: str
class UserOut(UserBase):
pass
class UserInDB(UserBase):
hashed_password: str
def fake_password_hasher(raw_password: str):
return "supersecret" + raw_password
def fake_save_user(user_in: UserIn):
hashed_password = fake_password_hasher(user_in.password)
user_in_db = UserInDB(**user_in.dict(), hashed_password=hashed_password)
print("User saved! ..not really")
return user_in_db
@app.post("/user/", response_model=UserOut)
async def create_user(user_in: UserIn):
user_saved = fake_save_user(user_in)
return user_saved
Union
或者 anyOf
¶
响应可以声明为两种类型的 Union
类型,即该响应可以是两种类型中的任意类型。
在 OpenAPI 中可以使用 anyOf
定义。
为此,请使用 Python 标准类型提示 typing.Union:
"笔记"
定义 Union 类型时,要把详细的类型写在前面,然后是不太详细的类型。下例中,更详细的 PlaneItem
位于 Union[PlaneItem,CarItem]
中的 CarItem
之前。
from typing import Union
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class BaseItem(BaseModel):
description: str
type: str
class CarItem(BaseItem):
type: str = "car"
class PlaneItem(BaseItem):
type: str = "plane"
size: int
items = {
"item1": {"description": "All my friends drive a low rider", "type": "car"},
"item2": {
"description": "Music is my aeroplane, it's my aeroplane",
"type": "plane",
"size": 5,
},
}
@app.get("/items/{item_id}", response_model=Union[PlaneItem, CarItem])
async def read_item(item_id: str):
return items[item_id]
模型列表¶
使用同样的方式也可以声明由对象列表构成的响应。
为此,请使用标准的 Python typing.List
:
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel):
name: str
description: str
items = [
{"name": "Foo", "description": "There comes my hero"},
{"name": "Red", "description": "It's my aeroplane"},
]
@app.get("/items/", response_model=list[Item])
async def read_items():
return items
任意 dict
构成的响应¶
任意的 dict
都能用于声明响应,只要声明键和值的类型,无需使用 Pydantic 模型。
事先不知道可用的字段 / 属性名时(Pydantic 模型必须知道字段是什么),这种方式特别有用。此时,可以使用 typing.Dict
:
from fastapi import FastAPI
app = FastAPI()
@app.get("/keyword-weights/", response_model=dict[str, float])
async def read_keyword_weights():
return {"foo": 2.3, "bar": 3.4}
小结¶
针对不同场景,可以随意使用不同的 Pydantic 模型继承定义的基类。
实体必须具有不同的状态时,不必为不同状态的实体单独定义数据模型。例如,用户实体就有包含 password
、包含 password_hash
以及不含密码等多种状态。
实践操作
响应单个模型
源代码
将代码文件写入responsedefault.py
from typing import Union
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel):
name: str
description: Union[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]
启动服务
uvicorn responsedefault:app --reload
浏览测试
浏览地址:http://127.0.0.1:8000/items/bar/name
浏览器输出:
{"name":"Bar","description":"The Bar fighters"}
多模型响应
源代码
代码写入responsemulti.py文件
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
class UserInDB(BaseModel):
username: str
hashed_password: str
email: EmailStr
full_name: str | None = None
def fake_password_hasher(raw_password: str):
return "supersecret" + raw_password
def fake_save_user(user_in: UserIn):
hashed_password = fake_password_hasher(user_in.password)
user_in_db = UserInDB(**user_in.dict(), hashed_password=hashed_password)
print("User saved! ..not really")
return user_in_db
@app.post("/user/", response_model=UserOut)
async def create_user(user_in: UserIn):
user_saved = fake_save_user(user_in)
return user_saved
启动服务
uvicron responsemulti:app --reload
浏览测试
在命令行使用curl
curl -X POST "http://127.0.0.1:8000/user/" -H "Content-Type: application/json" -d '{"username": "testuser", "password": "testpass", "email": "[email protected]", "full_name": "Test User" }'
输出:
curl -X POST "http://127.0.0.1:8000/user/" -H "Content-Type: application/json" -d '{"username": "testuser", "password": "testpass", "email": "[email protected]", "full_name": "Test User" }'
{"username":"testuser","email":"[email protected]","full_name":"Test User"}
总结
response_model
是「装饰器」方法(get
,post
等)的一个参数,会将输出数据限制在该模型定义内。
可以设置路径操作装饰器的 response_model_exclude_unset=True
参数,这样响应中将不会包含那些默认值,而是仅有实际设置的值。