如何使用FastAPI以JSON格式返回数据?

9

我已经在 FastAPIFlask 两个框架中编写了相同功能的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 中,字符串是:

"[\n    {\n        \"User\": \"aaa\",\n        \"date\": \"2022-09-26\",\n        \"count\": 840,\n]"

我使用 json.dumps() 的原因是 date 不可迭代。

在 FastAPI 中返回字符串,它将返回字符串。不要自己序列化 - 相反,返回对象,FastAPI 将为您序列化它。它应该可以很好地处理日期/时间:https://fastapi.tiangolo.com/tutorial/extra-data-types/ - MatsLindh
4个回答

13

错误的方法

如果你在返回对象之前对其进行序列化,使用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

第一种选择是像往常一样返回数据(例如dictlist等),即使用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 serializabledefault是一个函数,用于处理无法被序列化的对象。它应该返回对象的可JSON编码版本。在这种情况下,它是str,意味着所有不可序列化的对象都会被转换为字符串。如果你想以自定义方式序列化对象,你也可以使用自定义函数或JSONEncoder子类,如这里所示。此外,正如前面提到的选项1中所述,你还可以使用其他JSON编码器,例如orjson,与标准的json库相比,这可能会提高应用程序的性能(参见这个答案这个答案)。

注意3:FastAPI/Starlette的Response接受一个content参数,可以是strbytes对象。如示例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
    }
]

1

在@Chris的回答基础上进行深入探讨和建设:

简而言之:

对于第一种选项,如果您正在使用例如pandas,请首先执行例如:

JSONResponse(df.fillna(np.nan).replace([np.nan], [None]).to_dict())

对于第二个答案,不要使用缩进发送额外的空格,而是像这样:

Response(content=json_str, media_type='application/json')

原因:

如果您尝试发送任何NaN值,即使是长表或pandas上的一个值,第一个选项也会失败,因此它现在可能适用于您的尝试,但将来可能会失败(墨菲定律->将会失败)。修复方法从here开始。

对于第二部分,任何不为0的缩进都是为了人类消费,不会使您的代码运行更快。考虑到现代包甚至经常删除页面javascript的缩进。如果需要调试,则缩进消息是任何计算机都可以为您执行的操作,您喜欢的代码片段将愉快地缩进与您(观察者,而不是编写代码的人)舒适的空格数相同。

[根据评论更改答案。还要检查评论以获取更多信息。特别是如果流式传输。]


1
JSON编码器不允许NaN值。因此,要使用选项1,您需要替换DataFrame中的NaN值,例如使用df = df.fillna('')。由于您正在处理pandas DataFrame,您可能想在这里这里这里查看相关信息。 - Chris
好的观点,需要更多的研究来确定哪种解决方案是最好的...(特别是在流媒体时,在您的第三个链接上)...我现在不确定哪个选项是最好的。 - ntg

0
在路由中使用 fastapi response_class。
from fastapi.responses import JSONResponse

@app.get('/user', response_class=JSONResponse)
async def get_user():
    return some_dict

这个答案是错误的。无论如何,默认的response_class都是JSONResponse。请参考上面接受的答案以获取更多详细信息。 - undefined
@chris添加了一张截图,显示这个功能完全正常。强烈不建议不明确设置response_class。 - undefined
恐怕你弄错了。直接从文档中可以看到:"默认情况下,FastAPI将使用JSONResponse返回响应"。再次请仔细查看被接受的答案。另外,请避免发布代码、数据、响应消息的图片等 - 相反,请将文本复制或输入到答案中(更多详情请参见这里)。 - undefined

-1
此外,我经常处于接收请求的情况下,然后再进行另一个请求,并传递相同的参数以提供响应。在这些情况下,由于已从第一个请求中复制过来的“host”标头,第二个请求服务器通常不会接受该请求。
除此之外,有时我们需要使用json.loads来创建新的JSONResponse对象,以回答原始请求。否则,您将返回带有 JSON 和多个转义字符的字符串。
希望这种特定经验对某人有所帮助,如果您有更好的方法,请告诉我。

很抱歉,这个答案是错误的。请看一下上面被接受的答案,了解原因。 - undefined

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接