3 - Query Parameters and String Validation#
FastAPI allows us to declare additional information and validation about parameters.
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
import requests
url = 'http://127.0.0.1:8000'
params = {
'q': '1111 2222 3333 4444 5555 6666 7777 8888 9999 0000 1'
}
requests.get(url + '/items', params=params).json()
{'detail': [{'type': 'string_too_long',
'loc': ['query', 'q'],
'msg': 'String should have at most 50 characters',
'input': '1111 2222 3333 4444 5555 6666 7777 8888 9999 0000 1',
'ctx': {'max_length': 50}}]}
We can add more validations:
from 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
params = {
'q': '12'
}
requests.get(url + '/items', params=params).json()
{'detail': [{'type': 'string_too_short',
'loc': ['query', 'q'],
'msg': 'String should have at least 3 characters',
'input': '12',
'ctx': {'min_length': 3}}]}
Using Regular Expressions#
The regex below has the following construction:
^
means that the expression starts with the following characters$
means that there are no more characters after the preceding expression
In other words, the regex limits the query to be exactly fixedquery
.
@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
params = {
'q': 'fixquery'
}
requests.get(url + '/items', params=params).json()
{'detail': [{'type': 'string_pattern_mismatch',
'loc': ['query', 'q'],
'msg': "String should match pattern '^fixedquery$'",
'input': 'fixquery',
'ctx': {'pattern': '^fixedquery$'}}]}
params = {
'q': 'fixedquery'
}
requests.get(url + '/items', params=params).json()
{'items': [{'item_id': 'Foo'}, {'item_id': 'Bar'}], 'q': 'fixedquery'}
Multiple Values#
We can use list[]
to receive a list of items.
@app.get("/multiple_items/")
async def read_multiple_items(q: Annotated[list[str] | None, Query()] = None):
query_items = {"q": q}
return query_items
params = {
'q': [1, 2, 3]
}
requests.get(url + '/multiple_items', params=params).json()
{'q': ['1', '2', '3']}
Note that this is equivalent to this:
requests.get(url + '/multiple_items?q=1&q=2&q=3').json()
{'q': ['1', '2', '3']}
Adding More Metadata#
We can include more metadata using title
and description
that will be used when generating the Swagger docs.
@app.get("/items2/")
async def read_items2(
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#
Suppose we want a parameter to be item-query
, but item-query
is not a valid Python variable name. We can do this by declaring an alias:
@app.get("/items3/")
async def read_items(q: Annotated[str | None, Query(alias="item-query")] = None):
results = {"items3": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
if q:
results.update({"q": q})
return results
params = {
'item-query': 'asd5'
}
requests.get(url + '/items3', params=params).json()
{'items': [{'item_id': 'Foo'}, {'item_id': 'Bar'}], 'q': 'asd5'}
Deprecating Parameters#
If we want to allow a parameter, but signal to clients that it is being deprecated, we can set deprecated=True
. Note that this will only affect the Swagger Docs.
@app.get("/items4/")
async def read_items4(
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
params = {
'item-query': 'fixedquery'
}
requests.get(url + '/items4', params=params).json()
{'items': [{'item_id': 'Foo'}, {'item_id': 'Bar'}], 'q': 'fixedquery'}
Note that you can also set include_in_schema=False
to hide a parameter from the Swagger UI.
Custom Validation#
We can write coustom validation methods using Pydantic’s AfterValidator
inside of Annotated
.
import random
from pydantic import AfterValidator
data = {
"isbn-9781529046137": "The Hitchhiker's Guide to the Galaxy",
"imdb-tt0371724": "The Hitchhiker's Guide to the Galaxy",
"isbn-9781439512982": "Isaac Asimov: The Complete Stories, Vol. 2"
}
def check_valid_id(id: str):
if not id.startswith(("isbn-", "imdb-")):
raise ValueError('Invalid ID format, it must start with "isbn-" or "imdb-"')
return id
@app.get("/items5/")
async def read_items5(
id: Annotated[str | None, AfterValidator(check_valid_id)] = None,
):
if id:
item = data.get(id)
else:
id, item = random.choice(list(data.items()))
return {"id": id, "name": item}
params = {
'id': 'isbn-9781529046137'
}
requests.get(url + '/items5', params=params).json()
{'id': 'isbn-9781529046137', 'name': "The Hitchhiker's Guide to the Galaxy"}