2

I am exploring FastAPI, and got it working on my Docker Desktop on Windows. Here's my main.py which is deployed successfully in Docker:

#main.py
import fastapi
import json
from fastapi.responses import JSONResponse

app = fastapi.FastAPI()

@app.get('/api/get_weights1')
async def get_weights1():
    weights = {'aa': 10, 'bb': 20}
    return json.dumps(weights)

@app.get('/api/get_weights2')
async def get_weights2():
    weights = {'aa': 10, 'bb': 20}
    return JSONResponse(content=weights, status_code=200)

And I have a simple python file get_weights.py to make requests to those 2 APIs:

#get_weights.py
import requests
import json

resp = requests.get('http://127.0.0.1:8000/api/get_weights1')
print('ok', resp.status_code)
if resp.status_code == 200:
    print(resp.json())

resp = requests.get('http://127.0.0.1:8000/api/get_weights2')
print('ok', resp.status_code)
if resp.status_code == 200:
    print(resp.json())

I get the same responses from the 2 APIs, output:

ok 200
{"aa": 10, "bb": 20}
ok 200
{'aa': 10, 'bb': 20}

The response seems the same whether I use json.dumps() or JSONResponse(). I've read the FastAPI documentation on JSONResponse, but I still have below questions:

May I know if there is any difference between the 2 methods?

If there is a difference, which method is recommended (and why?)?

blackraven
  • 5,284
  • 7
  • 19
  • 45
  • To sum it up: don't use either, just return a dict-like object, or with `orm_mode=True` on your response model, an object that supports attribute lookup (an SQLAlchemy row, for example). Use `response_model` on the route decorator or give a return type for the function that defines how you want the response to be serialized. – MatsLindh Apr 21 '23 at 08:50
  • Thanks, the answer from @Matija is clear! – blackraven Apr 21 '23 at 15:16

2 Answers2

1

In FastAPI you can create response 3 different ways (from most concise to most flexible):

1

return dict # Or model or ...

In docs you linked we can see that FastAPI will automatically stringify this dict and wrap it in JSONResponse. This way is most concise and covers majority of use cases.

2

However sometimes you have to return custom headers (for example REMOTE-USER=username) or different status code (maybe 201 - Created or 202 - Accepted). This case you need to use JSONResponse.

return JSONResponse(content=dict) # Here we need to have dict.

Problem is that now if we don't have simple dict, we have to use jsonable_encoder(some_model) # -> dict to get it. So it's a tad more verbose. For available options check Starlette documentation, since FastAPI just reexports it.

More complex example:

return JSONResponse(content=jsonable_encoder(some_model), status_code=201, headers={"REMOTE-USER": username}) 

3

Finally you do not need to return json - you can also return csv, html or any other type of file. This case we have to use Response and specify media_type. Likewise use Starlette docs.

return Response('Hello, world!', media_type='text/plain')

In short

Note that Fastapi documentation states:

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

So we see what is difference: first method has good integration with other FastAPI functionality, so should always be preferred. Use second option only if you need to provide custom headers or status codes. Finally, use third option only if you want to return something that is not json.

Matija Sirk
  • 596
  • 2
  • 15
1

I've experimented a few variations and found the following...

(1) Both methods are not able to serialize a datetime object. For example if the weights are:

weights = {'aa': 10, 'bb': 20, 'date': datetime.date.today()}

then both methods will have the same returned status and error:

500 Internal Server Error
TypeError: Object of type date is not JSON serializable

To overcome this use

return json.dumps(weights, default=str)

and

from fastapi.encoders import jsonable_encoder
#blah
return JSONResponse(content=jsonable_encoder(weights), status_code=200)

(2) I've also experimented returning the plain dict as it is, especially if there is a datetime object in it. As @Matija has mentioned, FastAPI will automatically stringify this dict and wrap it in the response. For example:

@app.get('/api/get_weights1')
async def get_weights1():
    weights = {'aa': 10, 'bb': 20, 'date': datetime.date.today()}
    return weights    #<--- this is dict

Output:

ok 200
{"aa": 10, "bb": 20, "date": "2023-04-21"}

(3) As @Matija has mentioned, JSONResponse() method allows customization of the returned response. For example, the response status could be customized as 201 (instead of 200). And also different types of objects to be returned. This is probably the advantage of using this method over json.dumps() method. For example:

#main.py
@app.get('/api/get_weights2')
async def get_weights2():
    weights = {'aa': 10, 'bb': 20}
    return JSONResponse(content=weights, status_code=201)    #<--201

#get_weights.py
resp = requests.get('http://127.0.0.1:8000/api/get_weights2')
print('ok', resp.status_code)
if resp.status_code == 201:    #<---201
    print(resp.json())

Same output as before:

ok 200
{'aa': 10, 'bb': 20}
blackraven
  • 5,284
  • 7
  • 19
  • 45
  • 1
    I'm afraid you’re mistaken about `JSONResponse` and `json.dumps()`, as well as the assumption of how FastAPI/Starlette works under the hood. If you took the time to have a look at the link provided in the comments section above, your questions would be answered (as your question is essentially a duplicate one). @Matija's answer is also not very accurate, as there are more than three ways to return a `Response` (e.g., `StreamingResponse`, `HTMLResponse`, etc.), as well as one could also use other (faster) JSON encoders than the standard `json` lib. All is described in the linked answer above – Chris Apr 21 '23 at 15:42
  • thanks Chris, I'd have a closer read about how FastAPI/Starlette works! – blackraven Apr 21 '23 at 15:51
  • Please have a look at the link provided above. You might find [this answer](https://stackoverflow.com/a/71205127/17865804) (see Option 1) helpful as well. – Chris Apr 21 '23 at 16:08
  • the new link (Option 1) is clearer, I get the picture, thanks! – blackraven Apr 21 '23 at 16:14
  • 1
    ok done! Cheers! – blackraven Apr 21 '23 at 16:17