3

I have an endpoint which returns a Pydantic object. However, I would like a response code other than 200 in some cases (for example if my service in not healthy). How can I achieve that with FastAPI?

class ServiceHealth(BaseModel):
    http_ok: bool = True
    database_ok: bool = False

    def is_everything_ok(self) -> bool:
        return self.http_ok and self.database_ok

@router.get("/health")
def health() -> ServiceHealth:
    return ServiceHealth()
poiuytrez
  • 21,330
  • 35
  • 113
  • 172
  • Did you check https://fastapi.tiangolo.com/tutorial/handling-errors/#fastapis-httpexception-vs-starlettes-httpexception ? Something like: raise HTTPException(status_code=404, detail="Service not found.") can be useful for your intent ? – Madaray Mar 28 '23 at 14:38
  • Hum, I still would like to return a Pydantic object. – poiuytrez Mar 28 '23 at 15:38

2 Answers2

1

You can return a Response Directly.

For example, you can use JSONResponse and set the status manually:

@router.get("/health")
async def health() -> ServiceHealth:
    response = ServiceHealth()
    
    if response.is_everything_ok():
        return JSONResponse(content=response.dict(), status=200)
    return JSONResponse(content=response.dict(), status=500)

Also, there is a handy status.py which you can import from fastapi that contains all the available status codes:

from fastapi import status

@router.get("/health")
async def health() -> ServiceHealth:
    response = ServiceHealth()
    
    if response.is_everything_ok():
        return JSONResponse(content=response.dict(), status=status.HTTP_200_OK)
    return JSONResponse(content=response.dict(), status=status.HTTP_500_INTERNAL_SERVER_ERROR)
John Moutafis
  • 22,254
  • 11
  • 68
  • 112
  • 2
    When returning a `JSONResponse(content=some_dict, ....)`, please make sure that all objects in the dictionary that is being returned are JSON-serializable objects, otherwise a `TypeError: Object of type ... is not JSON serializable` would be raised. In that case, one should either convert such objects to a type that is JSON-serializable (e.g., `str`) on their own, or use FastAPI's `jsonable_encoder()` function that would automatically do this (see [this answer](https://stackoverflow.com/a/73974946/17865804) for more details and examples). – Chris Mar 30 '23 at 17:55
  • @Chris Yes, that is correct! Another thing you can do when you have a Pydantic object (with `datetimes` for example, that are not JSON serializable) in order to make sure they will be serialized correctly is to use the following combo: `json.load(pydantic_instance.json())` – John Moutafis Mar 31 '23 at 07:11
  • I wouldn't do that. Please have a look at the linked answer above, as well as [this answer](https://stackoverflow.com/a/71205127/17865804) (see Option 1) to find out the reason as to why. Simply, you are converting the model instance into JSON, then the JSON string/object into dictionary, and finally, once again into JSON, since `JSONResponse` will use `json.dumps()` behind the scenes (as explained in the links provided above). – Chris Mar 31 '23 at 07:31
  • You could instead use: `return Response(model.json(), media_type='application/json', status=status.HTTP_200_OK)`. See the linked answers above for more details. – Chris Mar 31 '23 at 07:35
1

Just specify the status_code keyword-argument, when initializing your APIRoute. It is passed along by all route decorators as far as I know, including APIRouter.get.

from fastapi import FastAPI
from pydantic import BaseModel


class ServiceHealth(BaseModel):
    http_ok: bool = True
    database_ok: bool = False


api = FastAPI()


@api.get("/health", status_code=299)
def health() -> ServiceHealth:
    return ServiceHealth()

Performing a GET on that route still returns the expected JSON {"http_ok":true,"database_ok":false} with the HTTP status code 299 (just to demonstrate, not a "real" status code).

There are a few restrictions that FastAPI places on that argument. Notably, you cannot return a body, if you define a 304 status code or any informational (1xx) status code.

Daniil Fajnberg
  • 12,753
  • 2
  • 10
  • 41