skip to Main Content

The design of my system consists of two parts:

  1. There is a FastAPI webserver interface which wraps some core logic
  2. There is a class which contains all of the core logic, let’s call it KeyValueStore for this example

The core logic can raise exceptions. I am not sure how I should be handling these in the FastAPI layer.

Let’s say that the core logic is as simple as a data structure which stores key-value pairs:

class KeyValueStore():
    def __init__(self):
        self.data = {}
    def put(self, key, value):
        if key in self.data:
            raise RuntimeError(f'duplicate key {key}')
        self.data[key] = value
    def get(self, key):
        # doesn't really matter what the implementation is

As you can see, if KeyValueStore.put() is called with a key which has previously been used, this results in an error of time RuntimeError being raised.

This is caught in the FastAPI layer. The code looks something like this:

from fastapi import FastAPI
from fastapi.responses import JSONResponse
from fastapi import status
from pydantic import BaseModel

class FastAPI_KeyValuePair(BaseModel):
    key: str
    value: str

class FastAPI_ReturnStatus(BaseModel):
    status: str
    message: str|None = None

@app.post('/put_key_value')
def put_key_value(key_value_pair: FastAPI_KeyValuePair):
    try:
        key_value_store.put(
            key_value_pair.key,
            key_value_pair.value,
        )
        return FastAPI_ReturnStatus(status='success', message=None)
    except RuntimeError as error:
        return JSONResponse(
            status_code=status.HTTP_409_CONFLICT,
            content=FastAPI_ReturnStatus(
                status='error',
                message=str(error),
            )
        )

In this particular case, I want to return a 409 Conflict status code, but I wasn’t sure how to do this while also returning some JSON body.

A normal return statement looks something like this:

return FastAPI_ReturnStatus(status='success', message=None)

If I understand correctly, FastAPI will serialize this type to JSON using json.dumps. This serialized content will be returned as the response body. In addition, the status code is automatically set to 200.

What I wanted to do was set this status code to something else, such as 409. I couldn’t find a way to do that, except by using JSONResponse:

return JSONResponse(
    status_code=status.HTTP_409_CONFLICT,
    content=FastAPI_ReturnStatus(
        status='error',
        message=str(error),
    )
)

I do not know if this is really the right approach. I just chose to use JSONResponse as there didn’t seem to be any other way to return a JSON body, and set an error status code (in this case, 409).

What actually happens with this code is that I get an exception:

TypeError: Object of type FastAPI_ReturnStatus is not JSON serializable

This confuses me, because this type is JSON serializable. It must be, because the line:

return FastAPI_ReturnStatus(status='success', message=None)

works.

Why is FastAPI_ReturnStatus not serializable in this context, but is serializable if the return type is FastAPI_ReturnStatus (not nested in some other object)?

2

Answers


  1. You can set a custom status code by including the default response in the list of parameters to your controller function:

    from fastapi import Response
    
    @app.post('/put_key_value')
    def put_key_value(key_value_pair: FastAPI_KeyValuePair, response: Response):
        try:
            key_value_store.put(
                key_value_pair.key,
                key_value_pair.value,
            )
    
            return FastAPI_ReturnStatus(status='success', message=None)
        except RuntimeError as error:
            response.status_code = 409
    
            return FastAPI_ReturnStatus(
                status='error',
                message=str(error),
            )
    

    Since this is the response object that will be used unless you return a custom Response object yourself, the status code will now be 409 if an error occurs instead of the default 200.

    Login or Signup to reply.
  2. First, I would highly suggest having a look at this answer and this answer, which would help you understand how FastAPI deals with responses (and serialisation, behind the scenes), and hence, understand the nature of the error.

    As for returning a JSON response that includes a Pydantic BaseModel and a custom status_code (again, please have a look at the linked answers above for more details), one could choose between the following options.

    Option 1

    Use the Response object to change the status_code, before returning the model as is.

    from fastapi import FastAPI, Response, status
    from pydantic import BaseModel
    
    
    class MyModel(BaseModel):
        msg: str
        
    
    app = FastAPI()
    
    
    @app.get('/')
    async def main(response: Response):
        response.status_code = status.HTTP_409_CONFLICT  # or simply = 409
        return MyModel(msg="test")
    

    Option 2

    Return a JSONResponse, after serialising the model using either FastAPI’s jsonable_encoder or other (likely faster) JSON encoders, as described in this answer.

    from fastapi import FastAPI, status
    from fastapi.encoders import jsonable_encoder
    from fastapi.responses import JSONResponse
    from pydantic import BaseModel
    
    
    class MyModel(BaseModel):
        msg: str
        
    
    app = FastAPI()
    
    
    @app.get('/')
    async def main():
        return JSONResponse(content=jsonable_encoder(MyModel(msg="test")), status_code=status.HTTP_409_CONFLICT)
    

    Option 3

    Alternatively, you could raise an HTTPException as follows.

    from fastapi import FastAPI, HTTPException, status
    from fastapi.encoders import jsonable_encoder
    from pydantic import BaseModel
    
    
    class MyModel(BaseModel):
        msg: str
        
    
    app = FastAPI()
    
    
    @app.get('/')
    async def main():
        raise HTTPException(detail=jsonable_encoder(MyModel(msg="test")), status_code=status.HTTP_409_CONFLICT)
    

    One could also create a custom Response class, as demonstrated in this answer.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search