FastAPI allows you to declare additional information and validation for your parameters.
Let's take this application as example:Python 3.10+Python 3.8+from fastapi import FastAPI app = FastAPI() @app.get("/items/") async def read_items(q: str | None = None): results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} if q: results.update({"q": q}) return results
The query parameter q
is of type Union[str, None]
(or str | None
in Python 3.10), that means that it’s of type str
but could also be None
, and indeed, the default value is None
, so FastAPI will know it’s not required.
Note
FastAPI will know that the value of q
is not required because of the default value = None
.
The Union
in Union[str, None]
will allow your editor to give you better support and detect errors.
Additional validation
We are going to enforce that even though q
is optional, whenever it is provided, its length doesn’t exceed 50 characters.
Import Query
and Annotated
¶
To achieve that, first import:
Query
fromfastapi
Annotated
fromtyping
(or fromtyping_extensions
in Python below 3.9)
Python 3.10+Python 3.8+
In Python 3.9 or above, Annotated
is part of the standard library, so you can import it from typing
.from typing import Annotated from fastapi import FastAPI, Query app = FastAPI() @app.get("/items/") async def read_items(q: Annotated[str | None, Query(max_length=50)] = None): results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} if q: results.update({"q": q}) return results
Info
FastAPI added support for Annotated
(and started recommending it) in version 0.95.0.
If you have an older version, you would get errors when trying to use Annotated
.
Make sure you Upgrade the FastAPI version to at least 0.95.1 before using Annotated
.
Use Annotated
in the type for the q
parameter¶
Remember I told you before that Annotated
can be used to add metadata to your parameters in the Python Types Intro?
Now it’s the time to use it with FastAPI. 🚀
We had this type annotation:Python 3.10+Python 3.8+q: str | None = None
What we will do is wrap that with Annotated
, so it becomes:Python 3.10+Python 3.8+q: Annotated[str | None] = None
Both of those versions mean the same thing, q
is a parameter that can be a str
or None
, and by default, it is None
.
Now let’s jump to the fun stuff. 🎉
Add Query
to Annotated
in the q
parameter
Now that we have this Annotated
where we can put more metadata, add Query
to it, and set the parameter max_length
to 50:Python 3.10+Python 3.8+from typing import Annotated from fastapi import FastAPI, Query app = FastAPI() @app.get("/items/") async def read_items(q: Annotated[str | None, Query(max_length=50)] = None): results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} if q: results.update({"q": q}) return results
Notice that the default value is still None
, so the parameter is still optional.
But now, having Query(max_length=50)
inside of Annotated
, we are telling FastAPI that we want it to extract this value from the query parameters (this would have been the default anyway 🤷) and that we want to have additional validation for this value (that’s why we do this, to get the additional validation). 😎
FastAPI will now:
- Validate the data making sure that the max length is 50 characters
- Show a clear error for the client when the data is not valid
- Document the parameter in the OpenAPI schema path operation (so it will show up in the automatic docs UI)
Alternative (old) Query
as the default value¶
Previous versions of FastAPI (before 0.95.0) required you to use Query
as the default value of your parameter, instead of putting it in Annotated
, there’s a high chance that you will see code using it around, so I’ll explain it to you.
Tip
For new code and whenever possible, use Annotated
as explained above. There are multiple advantages (explained below) and no disadvantages. 🍰
This is how you would use Query()
as the default value of your function parameter, setting the parameter max_length
to 50:Python 3.10+Python 3.8+from fastapi import FastAPI, Query app = FastAPI() @app.get("/items/") async def read_items(q: str | None = Query(default=None, max_length=50)): results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} if q: results.update({"q": q}) return results
As in this case (without using Annotated
) we have to replace the default value None
in the function with Query()
, we now need to set the default value with the parameter Query(default=None)
, it serves the same purpose of defining that default value (at least for FastAPI).
So:q: Union[str, None] = Query(default=None)
…makes the parameter optional, with a default value of None
, the same as:q: Union[str, None] = None
And in Python 3.10 and above:q: str | None = Query(default=None)
…makes the parameter optional, with a default value of None
, the same as:q: str | None = None
But it declares it explicitly as being a query parameter.
Info
Keep in mind that the most important part to make a parameter optional is the part:= None
or the:= Query(default=None)
as it will use that None
as the default value, and that way make the parameter not required.
The Union[str, None]
part allows your editor to provide better support, but it is not what tells FastAPI that this parameter is not required.
Then, we can pass more parameters to Query
. In this case, the max_length
parameter that applies to strings:q: Union[str, None] = Query(default=None, max_length=50)
This will validate the data, show a clear error when the data is not valid, and document the parameter in the OpenAPI schema path operation.
Query
as the default value or in Annotated
¶
Keep in mind that when using Query
inside of Annotated
you cannot use the default
parameter for Query
.
Instead use the actual default value of the function parameter. Otherwise, it would be inconsistent.
For example, this is not allowed:q: Annotated[str, Query(default="rick")] = "morty"
…because it’s not clear if the default value should be "rick"
or "morty"
.
So, you would use (preferably):q: Annotated[str, Query()] = "rick"
…or in older code bases you will find:q: str = Query(default="rick")
Advantages of Annotated
¶
Using Annotated
is recommended instead of the default value in function parameters, it is better for multiple reasons. 🤓
The default value of the function parameter is the actual default value, that’s more intuitive with Python in general. 😌
You could call that same function in other places without FastAPI, and it would work as expected. If there’s a required parameter (without a default value), your editor will let you know with an error, Python will also complain if you run it without passing the required parameter.
When you don’t use Annotated
and instead use the (old) default value style, if you call that function without FastAPI in other place, you have to remember to pass the arguments to the function for it to work correctly, otherwise the values will be different from what you expect (e.g. QueryInfo
or something similar instead of str
). And your editor won’t complain, and Python won’t complain running that function, only when the operations inside error out.
Because Annotated
can have more than one metadata annotation, you could now even use the same function with other tools, like Typer.
Add more validations
You can also add a parameter min_length
:Python 3.10+Python 3.9+Python 3.8+Python 3.10+ non-AnnotatedPython 3.8+ non-Annotatedfrom typing import Annotated from fastapi import FastAPI, Query app = FastAPI() @app.get("/items/") async def read_items( q: Annotated[str | None, Query(min_length=3, max_length=50)] = None ): results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} if q: results.update({"q": q}) return results
Add regular expressions
You can define a regular expression pattern
that the parameter should match:Python 3.10+Python 3.9+Python 3.8+Python 3.10+ non-AnnotatedPython 3.8+ non-Annotatedfrom typing import Annotated from fastapi import FastAPI, Query app = FastAPI() @app.get("/items/") async def read_items( q: Annotated[ str | None, Query(min_length=3, max_length=50, pattern="^fixedquery$") ] = None, ): results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} if q: results.update({"q": q}) return results
This specific regular expression pattern checks that the received parameter value:
^
: starts with the following characters, doesn’t have characters before.fixedquery
: has the exact valuefixedquery
.$
: ends there, doesn’t have any more characters afterfixedquery
.
If you feel lost with all these “regular expression” ideas, don’t worry. They are a hard topic for many people. You can still do a lot of stuff without needing regular expressions yet.
But whenever you need them and go and learn them, know that you can already use them directly in FastAPI.
Pydantic v1 regex
instead of pattern
¶
Before Pydantic version 2 and before FastAPI 0.100.0, the parameter was called regex
instead of pattern
, but it’s now deprecated.
You could still see some code using it:Python 3.10+ Pydantic v1from typing import Annotated from fastapi import FastAPI, Query app = FastAPI() @app.get("/items/") async def read_items( q: Annotated[ str | None, Query(min_length=3, max_length=50, regex="^fixedquery$") ] = None, ): results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} if q: results.update({"q": q}) return results
But know that this is deprecated and it should be updated to use the new parameter pattern
. 🤓
Default values¶
You can, of course, use default values other than None
.
Let's say that you want to declare the q
query parameter to have a min_length
of 3
, and to have a default value of "fixedquery"
:Python 3.9+Python 3.8+Python 3.8+ non-Annotatedfrom typing import Annotated from fastapi import FastAPI, Query app = FastAPI() @app.get("/items/") async def read_items(q: Annotated[str, Query(min_length=3)] = "fixedquery"): results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} if q: results.update({"q": q}) return results
Note
Having a default value of any type, including None
, makes the parameter optional (not required).
Make it required
When we don’t need to declare more validations or metadata, we can make the q
query parameter required just by not declaring a default value, like:q: str
instead of:q: Union[str, None] = None
But we are now declaring it with Query
, for example like:Annotatednon-Annotatedq: Annotated[Union[str, None], Query(min_length=3)] = None
So, when you need to declare a value as required while using Query
, you can simply not declare a default value:Python 3.9+Python 3.8+Python 3.8+ non-Annotatedfrom typing import Annotated from fastapi import FastAPI, Query app = FastAPI() @app.get("/items/") async def read_items(q: Annotated[str, Query(min_length=3)]): results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} if q: results.update({"q": q}) return results
Required with Ellipsis (...
)¶
There's an alternative way to explicitly declare that a value is required. You can set the default to the literal value ...
:Python 3.9+Python 3.8+Python 3.8+ non-Annotatedfrom typing import Annotated from fastapi import FastAPI, Query app = FastAPI() @app.get("/items/") async def read_items(q: Annotated[str, Query(min_length=3)] = ...): results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} if q: results.update({"q": q}) return results
Info
If you hadn’t seen that ...
before: it is a special single value, it is part of Python and is called “Ellipsis”.
It is used by Pydantic and FastAPI to explicitly declare that a value is required.
This will let FastAPI know that this parameter is required.
Required with None
You can declare that a parameter can accept None
, but that it’s still required. This would force clients to send a value, even if the value is None
.
To do that, you can declare that None
is a valid type but still use ...
as the default:Python 3.10+Python 3.9+Python 3.8+Python 3.10+ non-AnnotatedPython 3.8+ non-Annotatedfrom typing import Annotated from fastapi import FastAPI, Query app = FastAPI() @app.get("/items/") async def read_items(q: Annotated[str | None, Query(min_length=3)] = ...): results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} if q: results.update({"q": q}) return results
Tip
Pydantic, which is what powers all the data validation and serialization in FastAPI, has a special behavior when you use Optional
or Union[Something, None]
without a default value, you can read more about it in the Pydantic docs about Required Optional fields.
Tip
Remember that in most of the cases, when something is required, you can simply omit the default, so you normally don’t have to use ...
.
Query parameter list / multiple values
When you define a query parameter explicitly with Query
you can also declare it to receive a list of values, or said in other way, to receive multiple values.
For example, to declare a query parameter q
that can appear multiple times in the URL, you can write:Python 3.10+Python 3.9+Python 3.8+Python 3.10+ non-AnnotatedPython 3.9+ non-AnnotatedPython 3.8+ non-Annotatedfrom typing import Annotated from fastapi import FastAPI, Query app = FastAPI() @app.get("/items/") async def read_items(q: Annotated[list[str] | None, Query()] = None): query_items = {"q": q} return query_items
Then, with a URL like:http://localhost:8000/items/?q=foo&q=bar
you would receive the multiple q
query parameters’ values (foo
and bar
) in a Python list
inside your path operation function, in the function parameter q
.
So, the response to that URL would be:{ "q": [ "foo", "bar" ] }
Tip
To declare a query parameter with a type of list
, like in the example above, you need to explicitly use Query
, otherwise it would be interpreted as a request body.
The interactive API docs will update accordingly, to allow multiple values:
Query parameter list / multiple values with defaults
And you can also define a default list
of values if none are provided:Python 3.9+Python 3.8+Python 3.9+ non-AnnotatedPython 3.8+ non-Annotatedfrom typing import Annotated from fastapi import FastAPI, Query app = FastAPI() @app.get("/items/") async def read_items(q: Annotated[list[str], Query()] = ["foo", "bar"]): query_items = {"q": q} return query_items
If you go to:http://localhost:8000/items/
the default of q
will be: ["foo", "bar"]
and your response will be:{ "q": [ "foo", "bar" ] }
Using list
You can also use list
directly instead of List[str]
(or list[str]
in Python 3.9+):Python 3.9+Python 3.8+Python 3.8+ non-Annotatedfrom typing import Annotated from fastapi import FastAPI, Query app = FastAPI() @app.get("/items/") async def read_items(q: Annotated[list, Query()] = []): query_items = {"q": q} return query_items
Note
Keep in mind that in this case, FastAPI won’t check the contents of the list.
For example, List[int]
would check (and document) that the contents of the list are integers. But list
alone wouldn’t.
Declare more metadata
You can add more information about the parameter.
That information will be included in the generated OpenAPI and used by the documentation user interfaces and external tools.
Note
Keep in mind that different tools might have different levels of OpenAPI support.
Some of them might not show all the extra information declared yet, although in most of the cases, the missing feature is already planned for development.
You can add a title
:Python 3.10+Python 3.9+Python 3.8+Python 3.10+ non-AnnotatedPython 3.8+ non-Annotatedfrom typing import Annotated from fastapi import FastAPI, Query app = FastAPI() @app.get("/items/") async def read_items( q: Annotated[str | None, Query(title="Query string", min_length=3)] = None ): results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} if q: results.update({"q": q}) return results
And a description
:Python 3.10+Python 3.9+Python 3.8+Python 3.10+ non-AnnotatedPython 3.8+ non-Annotatedfrom typing import Annotated from fastapi import FastAPI, Query app = FastAPI() @app.get("/items/") async def read_items( q: Annotated[ str | None, Query( title="Query string", description="Query string for the items to search in the database that have a good match", min_length=3, ), ] = None, ): results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} if q: results.update({"q": q}) return results
Alias parameters¶
Imagine that you want the parameter to be item-query
.
Like in:http://127.0.0.1:8000/items/?item-query=foobaritems
But item-query
is not a valid Python variable name.
The closest would be item_query
.
But you still need it to be exactly item-query
…
Then you can declare an alias
, and that alias is what will be used to find the parameter value:Python 3.10+Python 3.9+Python 3.8+Python 3.10+ non-AnnotatedPython 3.8+ non-Annotatedfrom typing import Annotated from fastapi import FastAPI, Query app = FastAPI() @app.get("/items/") async def read_items(q: Annotated[str | None, Query(alias="item-query")] = None): results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} if q: results.update({"q": q}) return results
Deprecating parameters¶
Now let’s say you don’t like this parameter anymore.
You have to leave it there a while because there are clients using it, but you want the docs to clearly show it as deprecated.
Then pass the parameter deprecated=True
to Query
:Python 3.10+Python 3.9+Python 3.8+Python 3.10+ non-AnnotatedPython 3.8+ non-Annotatedfrom typing import Annotated from fastapi import FastAPI, Query app = FastAPI() @app.get("/items/") async def read_items( q: Annotated[ str | None, Query( alias="item-query", title="Query string", description="Query string for the items to search in the database that have a good match", min_length=3, max_length=50, pattern="^fixedquery$", deprecated=True, ), ] = None, ): results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} if q: results.update({"q": q}) return results
The docs will show it like this:
Exclude from OpenAPI¶
To exclude a query parameter from the generated OpenAPI schema (and thus, from the automatic documentation systems), set the parameter include_in_schema
of Query
to False
:Python 3.10+Python 3.9+Python 3.8+Python 3.10+ non-AnnotatedPython 3.8+ non-Annotatedfrom typing import Annotated from fastapi import FastAPI, Query app = FastAPI() @app.get("/items/") async def read_items( hidden_query: Annotated[str | None, Query(include_in_schema=False)] = None ): if hidden_query: return {"hidden_query": hidden_query} else: return {"hidden_query": "Not found"}
Leave a Reply