错误的方法
如果你在返回对象之前对其进行序列化,使用json.dumps()
(如你的示例中所示):
import json
@app.get('/user')
async def get_user():
return json.dumps(some_dict, indent=4, default=str)
返回的JSON对象
将被序列化两次,因为FastAPI会在幕后
自动对返回值进行序列化。这就是你得到输出字符串的原因。
"[\n {\n \"User\": \"aaa\",\n \"date\": \"2022-09-26\",\n ...
解决方案
请查看下面提供的解决方案,以及有关FastAPI/Starlette内部工作原理的解释。
选项1
第一种选择是像往常一样返回数据(例如dict
、list
等),即使用return some_dict
,然后FastAPI将在幕后自动将该返回值转换为JSON,并首先使用jsonable_encoder
将数据转换为与JSON兼容的数据。 jsonable_encoder
确保不可序列化的对象,如datetime
对象,转换为str
。然后,FastAPI将将该与JSON兼容的数据放入JSONResponse
中,该响应会以application/json
编码形式返回给客户端(这也在本答案的选项1中有解释)。可以在Starlette的源代码这里中看到JSONResponse
将使用Python标准的json.dumps()
对dict
进行序列化(有关备选/更快的JSON编码器,请参见此答案和此答案)。
例子
from datetime import date
d = [
{"User": "a", "date": date.today(), "count": 1},
{"User": "b", "date": date.today(), "count": 2},
]
@app.get('/')
def main():
return d
以上相当于:
from fastapi.responses import JSONResponse
from fastapi.encoders import jsonable_encoder
@app.get('/')
def main():
return JSONResponse(content=jsonable_encoder(d))
输出:
[{"User":"a","date":"2022-10-21","count":1},{"User":"b","date":"2022-10-21","count":2}]
直接返回一个
JSONResponse
或自定义的
Response
(如下面的选项2所示),以及任何继承自
Response
的其他响应类(请参阅FastAPI的文档
这里,以及Starlette的文档
这里和响应的实现
这里),也可以允许用户指定一个
自定义status_code
。FastAPI/Starlette的
JSONResponse
类的实现可以在
这里找到,还可以查看可用的HTTP状态码列表(而不是直接传递
HTTP响应状态码作为
int
)
这里。示例:
from fastapi import status
from fastapi.responses import JSONResponse
from fastapi.encoders import jsonable_encoder
@app.get('/')
def main():
return JSONResponse(content=jsonable_encoder(d), status_code=status.HTTP_201_CREATED)
选项2
如果因为任何原因(例如,尝试强制使用某些自定义JSON格式),您必须在返回对象之前对其进行序列化,那么您可以直接返回一个自定义的Response
,如此答案所述。根据文档:
当您直接返回Response
时,其数据不会被验证、转换(序列化)或自动记录。
此外,如此处所述:
FastAPI(实际上是Starlette)将自动包含Content-Length头。它还将根据media_type
并为文本类型附加字符集来包含Content-Type头。
因此,您还可以将
media_type
设置为您期望的数据类型;在这种情况下,即为
application/json
。以下是示例。
注意1:此答案中发布的JSON输出(选项1和2)是通过直接在浏览器中访问API端点(即在浏览器的地址栏中输入URL,然后按回车键)而得到的结果。如果您通过Swagger UI在
/docs
处测试端点,您会发现缩进不同(两个选项都是如此)。这是由于Swagger UI如何格式化
application/json
响应的方式。如果您也需要在Swagger UI上强制使用自定义缩进,您可以避免为下面示例中的
Response
指定
media_type
。这将导致内容显示为
文本,因为响应中将缺少
Content-Type
头,因此Swagger UI无法识别数据的类型以进行自定义格式化(对于
application/json
响应)。
注意2:在
json.dumps()
中将
default
参数设置为
str
是使得序列化
date
对象成为可能的关键,否则如果没有设置,你会得到:
TypeError: Object of type date is not JSON serializable
。
default
是一个函数,用于处理无法被序列化的对象。它应该返回对象的可JSON编码版本。在这种情况下,它是
str
,意味着所有不可序列化的对象都会被转换为字符串。如果你想以自定义方式序列化对象,你也可以使用自定义函数或
JSONEncoder
子类,如
这里所示。此外,正如前面提到的选项1中所述,你还可以使用其他JSON编码器,例如
orjson
,与标准的
json
库相比,这可能会提高应用程序的性能(参见
这个答案和
这个答案)。
注意3:FastAPI/Starlette的
Response
接受一个
content
参数,可以是
str
或
bytes
对象。如示例
here所示,如果不传递
bytes
对象,Starlette将尝试使用
content.encode(self.charset)
进行编码。因此,例如,如果传递了一个
dict
,你将得到:
AttributeError: 'dict' object has no attribute 'encode'
。在下面的示例中,传递了一个JSON
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('/')
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
}
]