custom fastapi query parameter validation

Viewed 799

Is there any way to have custom validation logic in a FastAPI query parameter?

example

I have a FastAPI app with a bunch of request handlers taking Path components as query parameters. For example:

def _raise_if_non_relative_path(path: Path):
    if path.is_absolute():
        raise HTTPException(
            status_code=409,
            detail=f"Absolute paths are not allowed, {path} is absolute."
        )

@app.get("/new",)
def new_file(where: Path):
    _raise_if_non_relative_path(where)
    # do save a file
    return Response(status_code=requests.codes.ok)

@app.get("/new",)
def delete_file(where: Path):
    _raise_if_non_relative_path(where)
    # do save a file
    return Response(status_code=requests.codes.ok)

I was wondering if there is way to ensure that the handler is not even called when the given file path is absolute. Now I have to repeat myself with _raise_if_non_relative_path everywhere.

what I tried

  • fastapi.Query:
    This only allows very basic validation (string length and regex). I could define a absolute path regex in this example. But a regex solution is really not generic, I want to validate with a custom function.
  • Subclass pathlib.Path with validation logic in __init__:
    This doesn't work, the type given in the type signature is ignored, and the object in my handler is a regular pathlib.PosixPath.
  • Use @app.middleware:
    this can work but seems overkill since not all my request handlers deal with Path objects.
  • class RelativePath(pydantic.Basemodel):
    I.e. define a class with single path field, which I can validate however I want. Unfortunately, this does not work for query parameters. If I do this, the request handler insists on having a json content body. Or at least that is what the swagger docs say.
1 Answers

This is the kind of validation that the Depends dependency management function is well-suited for. It allows you to define dependencies for given view functions, and add logic to validate (and lazily create) those dependencies. This gives a set of composable dependencies that can be re-used in those views where they are required.

You can create a relative_where_query dependency, and then depend on that to perform any required validation:

from fastapi import Depends, FastAPI, Response, Query
from fastapi.exceptions import HTTPException
from pathlib import Path
import requests

app = FastAPI()

def relative_where_query(where: Path = Query(...)):
    if where.is_absolute():
        raise HTTPException(
            status_code=409,
            detail=f"Absolute paths are not allowed, {where} is absolute."
        )
        
    return where
    
@app.get("/new")
def new_file(where: Path = Depends(relative_where_query)):
    return Response(status_code=requests.codes.ok)

This gives small, easily readable (and understandable) view functions, while the dependency ("I need a relative path from the where query parameter") has been moved to its own definition.

You can then re-use this dependency in every view function that require a relative path from the where query parameter (and you can further decompose and recompose these dependencies further if necessary).

Related