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).
Update 2023-05-01: If you want to generalize this handling to decouple the field name from the validation function, you can do that by generating a dynamic dependency. This examples creates a dynamic base model with Pydantic to do it - but there's surely other ways.
Since we now raise the error inside a Pydantic validator, we add an exception handler to give a proper error response if the path isn't valid.
The magic itself happens inside path_validator - this function takes the name given to it, uses it as a field name in a create_model call - which dynamically creates a Pydantic model from its parameters. This model is then used as a dependency in the controller.
The name given as an argument to the function will be the actual parameter name - both when being called (i.e. in the URL in this case) and when the documentation is displayed. The path name in the controller function itself is only relevant inside the function, and isn't used in the documentation or as the query parameter (the field name in the model is used for that).
from fastapi import Depends, FastAPI, Response, Query, Request
from fastapi.exceptions import HTTPException
from fastapi.responses import JSONResponse
from pathlib import Path
from pydantic import create_model, validator
import requests
app = FastAPI()
class PathNotAbsoluteError(Exception):
pass
@app.exception_handler(PathNotAbsoluteError)
async def value_error_exception_handler(request: Request, exc: PathNotAbsoluteError):
return JSONResponse(
status_code=400,
content={"message": str(exc)},
)
def path_is_absolute(cls, value):
if not value.is_absolute():
raise PathNotAbsoluteError("Given path is not absolute")
return value
def path_validator(param_name):
validators = {
'pathname_validator': validator(param_name)(path_is_absolute)
}
return create_model(
"AbsolutePathQuery",
**{param_name: (Path, Query(...))},
__validators__=validators,
)
@app.get("/path")
def new_file(path: Path = Depends(path_validator("where"))):
return Response(status_code=requests.codes.ok)
You can now reuse this in any location you want by adding a dependency on a given parameter name:
def new_file(path: Path = Depends(path_validator("my_path"))):
...
def old_file(path: Path = Depends(path_validator("where"))):
...