How to return data in JSON format using FastAPI?

If you serialise the object before returning it—using, for instance, json.dumps(), as in your example—the object will end up being serialised twice, as FastAPI will automatically serialise the return value. Hence, the reason for the output string you ended up with. See available options below.

Option 1

You could normally return data such as dict, list, etc., and FastAPI would automatically convert that return value into JSON, after first converting the data into JSON-compatible data (e.g., a dict) using the jsonable_encoder. The jsonable_encoder ensures that objects that are not serializable, such as datetime objects, are converted to a str. Then, behind the scenes, FastAPI would put that JSON-compatible data inside of a JSONResponse, which will return an application/json encoded response to the client. The JSONResponse, as can be seen in Starlette’s source code here, will use the Python standard json.dumps() to serialise the dict (for alternatvie/faster JSON encoders, see this answer).

Data:

from datetime import date

d = [{'User': 'a', 'date': date.today(), 'count': 1},
        {'User': 'b', 'date':  date.today(), 'count': 2}]

API Endpoint:

@app.get("https://stackoverflow.com/")
def main():
    return d

The above is equivalent to:

from fastapi.responses import JSONResponse
from fastapi.encoders import jsonable_encoder

@app.get("https://stackoverflow.com/")
def main():
    return JSONResponse(content=jsonable_encoder(d))

Output:

[{"User":"a","date":"2022-10-21","count":1},{"User":"b","date":"2022-10-21","count":2}]

Option 2

If, for any reason (e.g., trying to force some custom JSON format), you have to serialise the object before returning it, you can then return a custom Response directly, as described in this answer. As per the documentation:

When you return a Response directly its data is not validated,
converted (serialized), nor documented automatically.

Additionally, as described here:

FastAPI (actually Starlette) will automatically include a
Content-Length header. It will also include a Content-Type header,
based on the media_type and appending a charset for text types.

Hence, you can also set the media_type to whatever type you are expecting the data to be; in this case, that is application/json. Example is given below.

Note 1: The JSON outputs posted in this answer (in both Options 1 & 2) are the result of accessing the API endpoint through the browser directly (i.e., by typing the URL in the address bar of the browser and then hitting the enter key). If you tested the endpoint through Swagger UI at /docs instead, you would see that the indentation differs (in both options). This is due to how Swagger UI formats application/json responses. If you needed to force your custom indentation on Swagger UI as well, you could avoid specifying the media_type for the Response in the example below. This would result in displaying the content as text, as the Content-Type header would be missing from the response, and hence, Swagger UI couldn’t recognise the type of the data, in order to format them.

Note 2: Setting the default argument to str in json.dumps() is what makes it possible to serialise the date object, otherwise if it wasn’t set, you would get: TypeError: Object of type date is not JSON serializable. The default is a function that gets called for objects that can’t otherwise be serialized. It should return a JSON-encodable version of the object. In this case it is str, meaning that every object that is not serializable, it is converted to string. You could also use a custom function or JSONEncoder subclass, as demosntrated here, if you would like to serialise an object in a custom way.

Note 3: FastAPI/Starlette’s Response accepts as a content argument either a str or bytes object. As shown in the implementation here, if you don’t pass a bytes object, Starlette will try to encode it using content.encode(self.charset). Hence, if, for instance, you passed a dict, you would get: AttributeError: 'dict' object has no attribute 'encode'. In the example below, a JSON str is passed, which will later be encoded into bytes (you could alternatively encode it yourself before passing it to the Response object).

API Endpoint:

from fastapi import Response
import json

@app.get("https://stackoverflow.com/")
def main():
    json_str = json.dumps(d, indent=4, default=str)
    return Response(content=json_str, media_type="application/json")

Output:

[
    {
        "User": "a",
        "date": "2022-10-21",
        "count": 1
    },
    {
        "User": "b",
        "date": "2022-10-21",
        "count": 2
    }
]

Leave a Comment