Skip to content

Query parameters

Query parameters are the named parameters that come after the ? in a URL:

http://example.com/some/paths/?skip=1&limit=2

This URL has two simple query parameters:

  • skip: value of 1
  • limit: value of 2

In Xpresso, these are extracted using FromQuery[...], which is an alias for Annoated[..., QueryParam()]. Since they are part of the URL, they are always received as strings. But just like with path parameters, Xpresso can extract them and parse them into Python types and data structrues:

from xpresso import App, FromQuery, Path

fake_items_db = [
    {"item_name": "Foo"},
    {"item_name": "Bar"},
    {"item_name": "Baz"},
]


async def read_items(
    skip: FromQuery[int] = 0, limit: FromQuery[int] = 2
):
    return fake_items_db[skip : skip + limit]


app = App(
    routes=[
        Path(
            path="/items/",
            get=read_items,
        ),
    ]
)

Now navigate to http://127.0.0.1:8000/items/?skip=1&limit=1 to see the query parameters being used to filter items. You should get the following response:

[
  {
    "item_name": "Bar"
  }
]

Default (optional) parameters

Unlike path parameters which are always required, query paramters can be optional. To make a query parameter optional, simply give the parameter a default value like we do for limit and skip. So, for our example above http://localhost:8000/items/?skip=0&limit=2 and http://localhost:8000/items/ will produce the same result:

[
  {
    "item_name": "Foo"
  },
  {
    "item_name": "Bar"
  }
]

Of course if you don't give the parameter a default value, it will be required and an error response will automatically be returned if it is missing. For example, given:

from typing import Dict

from xpresso import App, FromQuery, Path


async def double(input: FromQuery[int]) -> Dict[str, int]:
    return {"result": input * 2}


app = App(
    routes=[
        Path(
            path="/math/double",
            get=double,
        ),
    ]
)

If you navigate to http://localhost:8000/math/double/ you will get:

{
  "detail": [
    {
      "loc": [
        "query",
        "input"
      ],
      "msg": "Missing required query parameter",
      "type": "value_error"
    }
  ]
}

Nullable parameters

Just because a parameter has a default value does not mean that it is nullable. For example, http://localhost:8000/items/?limit= means that limit has a value of "", which is considered a null value by OpenAPI. If you actually want to accept null values, you can make None an acceptable value for your parameter either using Optional[...] or Union[..., None]. On Python 3.10 you can also do int | None.

Now if a null value is sent, it will be converted to None. It is even possible to have a nullable parameter with a non-null default value, for example to express "the default limit is 10, but you can request all items (no/null limit)":

from typing import Optional

from xpresso import App, FromQuery, Path

fake_items_db = [
    {"item_name": "Foo"},
    {"item_name": "Bar"},
    {"item_name": "Baz"},
]


async def read_item(
    skip: FromQuery[int] = 0, limit: FromQuery[Optional[int]] = 2
):
    if limit is None:
        limit = len(fake_items_db)
    return fake_items_db[skip : skip + limit]


app = App(
    routes=[
        Path(
            path="/items/",
            get=read_item,
        ),
    ]
)

If you navigate to http://localhost:8000/items/?limit= you will get all of the items back (3):

[
  {
    "item_name": "Foo"
  },
  {
    "item_name": "Bar"
  },
  {
    "item_name": "Baz"
  }
]

While not including the parameter (http://localhost:8000/items/) at all will give you the default (2):

[
  {
    "item_name": "Foo"
  },
  {
    "item_name": "Bar"
  }
]

Repeated query parameters

Query parameters can be repeated, for example ?param=1&param=2. You can extract these repeated query parmaeters into a Python list (List[int] in this case). To accept repeated query parameters and extract them into a list, just pass the list type into FromQuery[...]:

from typing import List

from xpresso import App, FromQuery, Path

fake_items_db = [
    {"item_name": "Foo"},
    {"item_name": "Bar"},
    {"item_name": "Baz"},
]


async def read_item(prefix: FromQuery[List[str]]):
    return [
        item
        for item in fake_items_db
        if all(item["item_name"].startswith(p) for p in prefix or [])
    ]


app = App(
    routes=[
        Path(
            path="/items/",
            get=read_item,
        ),
    ]
)

Warning

If no values are sent, you will get an empty list. To require at least one value, use parameter constraints, which you will learn about in the Paramter Constraints and Metadata section of the docs.

Object query parameters

For advanced use cases, Xpresso also supports object-valued query parameters. These can be extracted into a Pydantic model or a dictionary (including support for free-form query parameters).

from typing import Optional

from pydantic import BaseModel

from xpresso import App, FromQuery, Path


class Filter(BaseModel):
    prefix: str
    limit: int
    skip: int = 0


async def read_items(
    filter: FromQuery[Optional[Filter]],
) -> Optional[Filter]:
    return filter


app = App(
    routes=[
        Path(
            path="/items/",
            get=read_items,
        ),
    ]
)

Now if you navigate to http://127.0.0.1:8000/items/?prefix=Ba&limit=1 you will get back the following JSON response:

{
  "limit": 1,
  "prefix": "Ba",
  "skip": 0
}

Customizing deserialization

Xpresso supports the full OpenAPI parameter serialization spec. For example, let's change the example above so that the paramters get serialized as ?filter[prefix]=Ba&filter[limit]=1:

from typing import Optional

from pydantic import BaseModel

from xpresso import App, Path, QueryParam
from xpresso.typing import Annotated


class Filter(BaseModel):
    prefix: str
    limit: int
    skip: int = 0


async def read_items(
    filter: Annotated[
        Optional[Filter], QueryParam(style="deepObject")
    ]
) -> Optional[Filter]:
    return filter


app = App(
    routes=[
        Path(
            path="/items/",
            get=read_items,
        ),
    ]
)

Now if you navigate to http://127.0.0.1:8000/items/?filter[prefix]=Ba&filter[limit]=1 (note that the URL is URL encoded) you will get back the following JSON response:

{
  "limit": 1,
  "prefix": "Ba",
  "skip": 0
}

This particular serialization is useful to support multiple objects with the same field name without name collisions.