pythonjsonserializationfastapistarlette

How to specify the response_model in FastAPI on a non-default return?


I have the following route:

# 201 is the response from a creation
# 409 if it already exists
# The server SHOULD generate a payload that includes enough information for a user to recognize the source of the conflict.
@app.post("/users", status_code=status.HTTP_201_CREATED, response_model=schemas.UserResponse)
def create_user(user: schemas.UserCreate, db: Session = Depends(get_db)):

    # hash the password -- user.password
    user.password = utils.hash(user.password)
    new_user = models.User(**user.dict()) # get or create? -- https://stackoverflow.com/a/6078058/651174
    db.add(new_user)
    try:
        db.commit()
    except sqlalchemy.exc.IntegrityError:
        db.rollback()
        existing_user = db.query(models.User).filter(models.User.email == user.email).first()
        raise HTTPException(status.HTTP_409_CONFLICT, detail=existing_user)

    db.refresh(new_user) # the same as doing `RETURNING *`
    return new_user

The user object returns and is encoded fine when it doesn't hit the exception. However, when it does go through the exception, and executes:

raise HTTPException(status.HTTP_409_CONFLICT, detail=existing_user)

I get the following error:

TypeError: Object of type User is not JSON serializable

Is there a way to basically encode all responses using the model I specify in FastAPI?


Solution

  • The reason that your new_user object is returned successfully when the request doesn't hit the exception is that (as explained in the documentation):

    By default, FastAPI would automatically convert that return value to JSON using the jsonable_encoder explained in JSON Compatible Encoder. Then, behind the scenes, it would put that JSON-compatible data (e.g. a dict) inside of a JSONResponse that would be used to send the response to the client.

    As explained in this answer in more detail, when returning a value from an endpoint, FastAPI will first convert the value into JSON-compatible data using the jsonable_encoder, thus ensuring that any objects that are not serializable, such as datetime objects, are converted to a str. Hence, avoiding any potential errors, such as:

    TypeError: Object of type User is not JSON serializable
    

    when FastAPI will later put that data into a JSONResponse, which uses the standard json.dumps() function to convert the data into JSON (see the linked answer above for more details and references).

    However, when raising an HTTPException, which would actually return a JSONResponse directly without using the jsonable_encoder first to ensure that the data are JSON-compatible (you can check that through http_exception_handler), you would need to convert the existing_user object/model using jsonable_encoder by yourself, before passing it to the exception. As described in the documentation:

    You cannot put a Pydantic model in a JSONResponse without first converting it to a dict with all the data types (like datetime, UUID, etc) converted to JSON-compatible types.

    Hence, you should use:

    from fastapi.encoders import jsonable_encoder
    
    raise HTTPException(detail=jsonable_encoder(existing_user), status_code=status.HTTP_409_CONFLICT)