Search code examples
pythonfastapipydanticstarlette

FastAPI: get data into view even though data is invalid


In our app there is a view that accepts an instance of a model as an argument, and if the request data misses some fields, the view does not get called, eg:

class Item(BaseModel):
    id: int
    price: float
    is_offer: bool | None = False

@app.post("/")
async def hello_root(item: Item):
    return dict(item)

This was fine for quite a while, but now we need to add the item to the database even if some of the fields are missing, but we still need to be able to tell that the item is invalid so we don't do some other logic with it.

The problem is that if the item is invalid, the view does not get called at all. Also, we can't replace item: Item with item: dict in the view function signature for historic reasons.

I tried adding a custom exception handler, but then it applies for all the views and I would have to figure out which view would have been called, and then reuse some logic from this particular one, and getting the item data is not that straightforward either:

@app.exception_handler(RequestValidationError)
async def req_validation_handler(request, exc):
    print("We got an error")
    ...

My other idea was to create some sort of a custom field that could be nullable, but at the same time have a flag as to whether it is required or not which could be checked inside our view, but I still haven't figured out how to do that.

Is there a proper way of doing this?


Solution

  • Option 1

    One way would be to define item as dict/Body/Form type, or use the Request object directly (and call await request.json())—as demonstrated here and here—and perform the pydantic model validation check in a dependency function/class yourself, as described in Method 3 of this answer, which would allow you to tell whether or not the Item is valid and handle it as desired.

    Option 2

    Given that you may not prefer the approach in Option 1, as you may need to stick to the Pydantic model definition in your endpoint (as mentioned in the question), you could either use a separate router (with optionally a custom APIRoute class, in case you would like to handle this before the request reaches the endpoint), or a sub-application, and to that router or sub-app, add only the endpoint for which you would like to handle such ValidationError exceptions—using, for instance, @router.post('/') or @subapi.post('/')(examples of both options can be found here and here). Once you do that, use a custom exception handler for that router/sub-app (to handle exceptions being raised for the endpoint you added earlier), and catch/handle such exceptions occuring as desired—working examples can be found here, as well as here and here.