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"}