from fastapi import FastAPI, Query
app = FastAPI()
@app.get("/")
def func(m: str = Query(max_length=5)):
return '...'
I'm curious about the implementation of parameter validation. Is it done via decorator? I never knew decorators have this style. How does the Query
function know that it needs to validate the m
parameter and m
is a str
data type?
The route decorator (here @app.get
) analyses the signature of your handler function (here func
). In this case it finds a Query
object set as the default for parameter m
and constructs the final request handler function accordingly -- with the constraints defined in Query
.
Later, when a request comes in, that final request handler function validates the query parameters of the request against whatever was defined before, and either lets it through, if it passes validation, or raises an error, if it doesn't.
The following description is based on the public (albeit almost entirely undocumented) interface of the current stable version 0.95.1
of FastAPI. But I noticed before that some of the non-protected functions were modified, moved or trashed altogether, so I guess it would be safer to view most of the following stuff (aside from maybe the APIRoute
interface) as implementation details.
APIRoute
constructionWhen you apply one of those handy little @app.get
or @router.post
decorators, a lot of things actually happen under the hood, before the server is even up, and way before the first request can come in.
Ultimately that act of decoration will result in a call to APIRouter.add_api_route
(since the other methods like e.g. get
are essentially convenience wrappers around that).
That method will construct an instance of APIRoute
(or a subclass), which receives (among many other things) your endpoint/route handler function (func
in your example) as an argument and sets it as its endpoint
attribute. And inside the APIRoute.__init__
method that handler function will be passed to the get_dependant
function to construct what will later be necessary to validate your requests.
We'll take a look at that next.
When you call the Query(...)
function, you are constructing a Query
object. When you follow the inheritance tree, you'll find that Query
is ultimately just a subclass of Pydantic's FieldInfo
. That is just a bucket to hold all the constraints defined for a model field. This is one of those places, where FastAPI is tightly coupled to the Pydantic way of doing things.
So, your func
signature contains a default value for the m
parameter and that value is of the type Query
. As mentioned earlier, the route constructor calls get_dependant
passing your func
as its call
argument, and get_dependant
will promptly inspect its signature by calling get_typed_signature(call)
.
Now it has access to all the parameters of func
including their default values, which means it has that aforementioned Query
instance associated with the m
parameter.
ModelField
setupThe get_dependant
function will construct a Dependant
object and then it will iterate over the handler function parameters, get to m
and call analyze_param
with the info it has on that parameter, which will (among other things) return a Pydantic ModelField
instance for that parameter.
Unlike the simple FieldInfo
mentioned earlier, the ModelField
class is much more powerful and it implements the validation capabilities that are so essential for Pydantic models and that FastAPI cleverly utilizes for request validation.
Towards the end of get_dependant
the add_param_to_fields
function merely attaches this ModelField
instance to the Dependant
object's query_params
attribute (a list of fields).
It is important to note that the ModelField
object now already has everything it needs to validate any object against the constraints you defined in Query
. By the way, the type annotation for that route handler parameter (here str
) is also incorporated in the ModelField
validation logic at this point.
The very last step in APIRoute.__init__
is to set up an ASGI app for that route with all the information gathered up to this point by calling request_response(self.get_route_handler())
. The interesting part for us is the get_route_handler
call though, which is just a convenience wrapper around a call to get_request_handler
.
That function takes that previously constructed Dependant
object as well as many other things. It internally constructs and then returns that inconspicuously named app
function.
But that is where the magic happens. By that I mean that app
is the function that can be rightfully called the final request handler, i.e. it takes a Request
and it returns a Response
.
All the previous steps happen, when the module containing your route handler function is loaded, i.e. before any request can even begin to be processed. The following describes what happens later on, when a request actually comes.
In one of the stages of the aforementioned final request handler, the solve_dependencies
coroutine is awaited, which takes the Request
object and our Dependant
object. There the request_params_to_args
function is called with the query_parms
list of the Dependant
(i.e. how the query parameters should look) and the request's actual query parameters.
And that is where the validate
method of each ModelField
instance is called. This again is Pydantic territory and will ensure that the query parameters are of the right type, fit all our constraints, etc.
If no validation errors are encountered, the validated values along with all the rest are passed to your handler function (finally!) via run_endpoint_function
.
This is how you can be sure, that the string m
inside func
will ultimately be no more than 5
characters in length.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With