如何使用 FastAPI 以 JSON 格式返回数据?
- 2024-12-13 08:37:00
- admin 原创
- 100
问题描述:
我在FastAPI和Flask中编写了具有相同功能的相同 API 应用程序。但是,在返回 JSON 时,两个框架之间的数据格式不同。两者都使用相同的json
库,甚至完全相同的代码:
import json
from google.cloud import bigquery
bigquery_client = bigquery.Client()
@router.get('/report')
async def report(request: Request):
response = get_clicks_impression(bigquery_client, source_id)
return response
def get_user(client, source_id):
try:
query = """ SELECT * FROM ....."""
job_config = bigquery.QueryJobConfig(
query_parameters=[
bigquery.ScalarQueryParameter("source_id", "STRING", source_id),
]
)
query_job = client.query(query, job_config=job_config) # Wait for the job to complete.
result = []
for row in query_job:
result.append(dict(row))
json_obj = json.dumps(result, indent=4, sort_keys=True, default=str)
except Exception as e:
return str(e)
return json_obj
Flask中返回的数据是字典:
{
"User": "fasdf",
"date": "2022-09-21",
"count": 205
},
{
"User": "abd",
"date": "2022-09-27",
"count": 100
}
]
而在FastAPI中则是字符串:
"[
{
\"User\": \"aaa\",
\"date\": \"2022-09-26\",
\"count\": 840,
]"
我使用这个的原因json.dumps()
是date
无法令人发笑。
解决方案 1:
错误的方法
如果在返回对象之前对其进行序列化,则使用json.dumps()
(如示例所示),例如:
import json
@app.get('/user')
async def get_user():
return json.dumps(some_dict, indent=4, default=str)
返回的 JSON 对象最终将被序列化两次,因为在这种情况下,FastAPI 也会在后台自动序列化返回值。因此,最终得到输出字符串的原因是:
"[
{
\"User\": \"aaa\",
\"date\": \"2022-09-26\",
...
解决方案
查看可用的解决方案,以及下面有关 FastAPI/Starlette 如何在底层工作的解释。
选项 1
第一个选项是照常返回数据(例如dict
、list
等)—即,使用例如return some_dict
—并且 FastAPI 会在后台使用将数据转换为 JSON 兼容数据后,自动将该返回值转换为 JSONjsonable_encoder
。确保jsonable_encoder
将不可序列化的对象(例如datetime
对象)转换为str
。然后,FastAPI 会将 JSON 兼容数据放入里面JSONResponse
,它将application/json
向客户端返回编码的响应(这也在本答案的选项 1 中进行了解释)。从这里的JSONResponse
Starlette 的源代码中可以看出,将使用 Python 标准来序列化(有关替代/更快的 JSON 编码器,请参阅此答案和此答案)。json.dumps()
`dict`
例子
from datetime import date
d = [
{"User": "a", "date": date.today(), "count": 1},
{"User": "b", "date": date.today(), "count": 2},
]
@app.get('/')
async def main():
return d
以上相当于:
from fastapi.responses import JSONResponse
from fastapi.encoders import jsonable_encoder
@app.get('/')
async def main():
return JSONResponse(content=jsonable_encoder(d))
输出:
[{"User":"a","date":"2022-10-21","count":1},{"User":"b","date":"2022-10-21","count":2}]
改变status_code
要更改status_code
返回dict
对象的时间,您可以使用Response
对象,如文档中所述,如下所示:
from fastapi import Response, status
@app.get('/')
async def main(response: Response):
response.status_code = status.HTTP_201_CREATED # or simply = 201
return d
还可以在返回时指定自定义 或直接指定自定义(在下面的选项 2 中演示),以及从中继承的任何其他响应类(请参阅此处的FastAPI 文档,以及此处的Starlette 文档和此处的响应实现)。 FastAPI/Starlette 类的实现可在此处找到,以及可以使用的 HTTP 状态代码列表(而不是直接将HTTP 响应状态代码作为传递)可在此处看到。 例如:status_code
`JSONResponseResponse
ResponseJSONResponse
int`
from fastapi import status
from fastapi.responses import JSONResponse
from fastapi.encoders import jsonable_encoder
@app.get('/')
async def main():
return JSONResponse(content=jsonable_encoder(d), status_code=status.HTTP_201_CREATED)
选项 2
如果出于任何原因(例如,尝试强制使用某种自定义 JSON 格式),您必须在返回对象之前对其进行序列化,则您可以直接返回自定义对象,如此Response
答案中所述。 根据文档:
当您直接返回时,
Response
其数据不会被验证、转换(序列化)或自动记录。
此外,如下所述:
FastAPI(实际上是 Starlette)将自动包含一个 Content-Length 标头。它还将包含一个 Content-Type 标头,
media_type
并根据该标头附加文本类型的字符集。
因此,您还可以将 设置media_type
为您期望数据的任何类型;在本例中,即application/json
。示例如下。要选择性地更改对象status_code
的Response
,您可以在返回 时使用选项 1 中描述的相同方法JSONResponse
。
注 1:此答案中发布的 JSON 输出(在选项 1 和 2 中)是通过浏览器直接访问 API 端点的结果(即,在浏览器的地址栏中键入 URL,然后按 Enter 键)。如果您通过 Swagger UI 测试端点/docs
,则会看到缩进不同(在两个选项中)。这是由于 Swagger UI 格式化application/json
响应的方式造成的。如果您也需要在 Swagger UI 上强制自定义缩进,则可以避免在下面的示例中media_type
为指定Response
。这将导致将内容显示为文本,因为Content-Type
响应中缺少标题,因此 Swagger UI 无法识别数据的类型,以便自定义格式化它们(在响应的情况下application/json
)。
注 2:将default
参数设置为str
injson.dumps()
使得可以序列化date
对象,否则如果未设置,您将得到:TypeError: Object of type date is not JSON serializable
。default
是一个针对无法序列化的对象调用的函数。它应该返回对象的 JSON 编码版本。在这种情况下它是str
,这意味着每个不可序列化的对象都会转换为字符串。如果您想以自定义方式序列化对象,也可以使用自定义函数或JSONEncoder
子类,如此处演示的。此外,如前面选项 1 中提到的,人们可以使用其他 JSON 编码器,例如orjson
,与标准库相比,它可能会提高应用程序的性能json
(请参阅此答案和此答案)。
注意 3:FastAPI/StarletteResponse
接受content
astr
或object 作为参数。如此处bytes
的实现所示,如果您不传递对象,Starlette 将尝试使用 对其进行编码。因此,例如,如果您传递了 ,您将获得:。在下面的示例中,传递了一个 JSON,稍后将对其进行编码(您也可以在将其传递给对象之前自己对其进行编码)。bytes
`content.encode(self.charset)dict
AttributeError: 'dict' object has no attribute 'encode'str
bytes`Response
例子
from fastapi import Response
from datetime import date
import json
d = [
{"User": "a", "date": date.today(), "count": 1},
{"User": "b", "date": date.today(), "count": 2},
]
@app.get('/')
async def main():
json_str = json.dumps(d, indent=4, default=str)
return Response(content=json_str, media_type='application/json')
输出:
[
{
"User": "a",
"date": "2022-10-21",
"count": 1
},
{
"User": "b",
"date": "2022-10-21",
"count": 2
}
]
解决方案 2:
主要基于并建立在@Chris 的回答之上:
总结:
对于第一个选项,如果您首先使用 pandas,请执行以下操作:
JSONResponse(df.fillna(np.nan).replace([np.nan], [None]).to_dict())
对于第二个答案,
不要使用缩进发送多余的空格,而是这样做:
Response(content=json_str, media_type='application/json')
原因:
如果您尝试发送任何 nan 值,即使它是长表或 pandas 上的一个值,第一个选项也会失败,因此它现在可能适合您的尝试,但将来可能会失败(Murphy -> 将失败)。修复从这里开始。
对于第二部分,任何非 0 的缩进都是供人类使用的,不会帮助您的代码运行得更快。考虑到现代软件包甚至经常删除页面的 javascript 缩进。如果需要调试,任何计算机都会很乐意为您缩进消息,并且您最喜欢的代码段也会很乐意缩进您(观察者,而不是编写代码的人)感到舒适的空格数。
[根据评论更改了答案。另请查看评论以了解更多信息。尤其是流媒体。]
解决方案 3:
使用response_class=JSONResponse
:
from fastapi.responses import JSONResponse
@app.get("/db/{jobid}", include_in_schema=True, response_class=JSONResponse)
async def dbget(jobid:int):
""" read data stored on the jobid """
return db.read(f"jobid=={jobid}")
- 2024年20款好用的项目管理软件推荐,项目管理提效的20个工具和技巧
- 2024年开源项目管理软件有哪些?推荐5款好用的项目管理工具
- 2024年常用的项目管理软件有哪些?推荐这10款国内外好用的项目管理工具
- 项目管理软件有哪些?推荐7款超好用的项目管理工具
- 项目管理软件有哪些最好用?推荐6款好用的项目管理工具
- 项目管理软件哪个最好用?盘点推荐5款好用的项目管理工具
- 项目管理软件有哪些,盘点推荐国内外超好用的7款项目管理工具
- 项目管理软件排行榜:2024年项目经理必备5款开源项目管理软件汇总
- 项目管理必备:盘点2024年13款好用的项目管理软件
- 2024项目管理软件排行榜(10类常用的项目管理工具全推荐)