Parameters#

Path Parameters#

Path parameters are parameters declared as part of the path component of the URL. They are declared using a simple syntax {param_name:param_type} :

from pydantic import BaseModel

from starlite import Starlite, get

USER_DB = {1: {"id": 1, "name": "John Doe"}}


class User(BaseModel):
    id: int
    name: str


@get("/user/{user_id:int}")
def get_user(user_id: int) -> User:
    return User.parse_obj(USER_DB[user_id])


app = Starlite(route_handlers=[get_user])

In the above there are two components:

  1. The path parameter is defined in the @get decorator, which declares both the parameter’s name user_id) and type int.

  2. The decorated function get_user defines a parameter with the same name as the parameter defined in the path kwarg.

The correlation of parameter name ensures that the value of the path parameter will be injected into the function when it’s called.

Supported Path Parameter Types#

Currently, the following types are supported:

  • date: Accepts date strings and time stamps.

  • datetime: Accepts date-time strings and time stamps.

  • decimal: Accepts decimal values and floats.

  • float: Accepts ints and floats.

  • int: Accepts ints and floats.

  • path: Accepts valid POSIX paths.

  • str: Accepts all string values.

  • time: Accepts time strings with optional timezone compatible with pydantic formats.

  • timedelta: Accepts duration strings compatible with the pydantic formats.

  • uuid: Accepts all uuid values.

The types declared in the path parameter and the function do not need to match 1:1 - as long as parameter inside the function declaration is typed with a “higher” type to which the lower type can be coerced, this is fine. For example, consider this:

from datetime import datetime, timezone
from typing import List

from pydantic import BaseModel

from starlite import Starlite, get


class Order(BaseModel):
    id: int
    customer_id: int


ORDERS_BY_DATETIME = {
    datetime.fromtimestamp(1667924386, tz=timezone.utc): [
        Order(id=1, customer_id=2),
        Order(id=2, customer_id=2),
    ]
}


@get(path="/orders/{from_date:int}")
def get_orders(from_date: datetime) -> List[Order]:
    return ORDERS_BY_DATETIME[from_date]


app = Starlite(route_handlers=[get_orders])
from datetime import datetime, timezone

from pydantic import BaseModel

from starlite import Starlite, get


class Order(BaseModel):
    id: int
    customer_id: int


ORDERS_BY_DATETIME = {
    datetime.fromtimestamp(1667924386, tz=timezone.utc): [
        Order(id=1, customer_id=2),
        Order(id=2, customer_id=2),
    ]
}


@get(path="/orders/{from_date:int}")
def get_orders(from_date: datetime) -> list[Order]:
    return ORDERS_BY_DATETIME[from_date]


app = Starlite(route_handlers=[get_orders])
from datetime import datetime, UTC

from pydantic import BaseModel

from starlite import Starlite, get


class Order(BaseModel):
    id: int
    customer_id: int


ORDERS_BY_DATETIME = {
    datetime.fromtimestamp(1667924386, tz=UTC): [
        Order(id=1, customer_id=2),
        Order(id=2, customer_id=2),
    ]
}


@get(path="/orders/{from_date:int}")
def get_orders(from_date: datetime) -> list[Order]:
    return ORDERS_BY_DATETIME[from_date]


app = Starlite(route_handlers=[get_orders])

The parameter defined inside the path kwarg is typed as int , because the value passed as part of the request will be a timestamp in milliseconds without any decimals. The parameter in the function declaration though is typed as datetime.datetime. This works because the int value will be passed to a pydantic model representing the function signature, which will coerce the int into a datetime. Thus, when the function is called it will be called with a datetime typed parameter.

Note

You only need to define the parameter in the function declaration if it’s actually used inside the function. If the path parameter is part of the path, but the function doesn’t use it, it’s fine to omit it. It will still be validated and added to the openapi schema correctly.

Extra validation and documentation for path params#

If you want to add validation or enhance the OpenAPI documentation generated for a given path parameter, you can do so using the the parameter function:

from pydantic import BaseModel, Json, conint
from pydantic_openapi_schema.v3_1_0.example import Example
from pydantic_openapi_schema.v3_1_0.external_documentation import ExternalDocumentation

from starlite import Parameter, Starlite, get


class Version(BaseModel):
    id: conint(ge=1, le=10)  # type: ignore[valid-type]
    specs: Json


VERSIONS = {1: Version(id=1, specs='{"some": "value"}')}


@get(path="/versions/{version:int}")
def get_product_version(
    version: int = Parameter(
        ge=1,
        le=10,
        title="Available Product Versions",
        description="Get a specific version spec from the available specs",
        examples=[Example(value=1)],
        external_docs=ExternalDocumentation(
            url="https://mywebsite.com/documentation/product#versions",  # type: ignore[arg-type]
        ),
    )
) -> Version:
    return VERSIONS[version]


app = Starlite(route_handlers=[get_product_version])

In the above example, Parameter is used to restrict the value of version to a range between 1 and 10, and then set the title, description, examples and externalDocs sections of the OpenAPI schema.

Query Parameters#

Query parameters are defined as keyword arguments to handler functions. Every keyword argument that is not otherwise specified (for example as a path parameter) will be interpreted as a query parameter.

from typing import Dict

from starlite import Starlite, get


@get("/")
def index(param: str) -> Dict[str, str]:
    return {"param": param}


app = Starlite(route_handlers=[index])

# run: /?param=foo
# run: /?param=bar
from starlite import Starlite, get


@get("/")
def index(param: str) -> dict[str, str]:
    return {"param": param}


app = Starlite(route_handlers=[index])

# run: /?param=foo
# run: /?param=bar

Technical details

These parameters will be parsed from the function signature and used to generate a pydantic model. This model in turn will be used to validate the parameters and generate the OpenAPI schema.

This means that you can also use any pydantic type in the signature, and it will follow the same kind of validation and parsing as you would get from pydantic.

Query parameters come in three basic types:

  • Required

  • Required with a default value

  • Optional with a default value

Query parameters are required by default. If one such a parameter has no value, a ValidationException will be raised.

Settings defaults#

In this example, param will have the value "hello" if it’s not specified in the request. If it’s passed as a query parameter however, it will be overwritten:

from typing import Dict

from starlite import Starlite, get


@get("/")
def index(param: str = "hello") -> Dict[str, str]:
    return {"param": param}


app = Starlite(route_handlers=[index])


# run: /
# run: /?param=john
from starlite import Starlite, get


@get("/")
def index(param: str = "hello") -> dict[str, str]:
    return {"param": param}


app = Starlite(route_handlers=[index])


# run: /
# run: /?param=john

Optional parameters#

Instead of only setting a default value, it’s also possible to make a query parameter entirely optional.

Here, we give a default value of None , but still declare the type of the query parameter to be a string. This means that this parameter is not required. If it is given, it has to be a string. If it is not given, it will have a default value of None

from typing import Dict, Optional

from starlite import Starlite, get


@get("/")
def index(param: Optional[str] = None) -> Dict[str, Optional[str]]:
    return {"param": param}


app = Starlite(route_handlers=[index])


# run: /
# run: /?param=goodbye
from typing import Optional

from starlite import Starlite, get


@get("/")
def index(param: Optional[str] = None) -> dict[str, Optional[str]]:
    return {"param": param}


app = Starlite(route_handlers=[index])


# run: /
# run: /?param=goodbye
from starlite import Starlite, get


@get("/")
def index(param: str | None = None) -> dict[str, str | None]:
    return {"param": param}


app = Starlite(route_handlers=[index])


# run: /
# run: /?param=goodbye

Type coercion#

It is possible to coerce query parameters into different types. A query starts out as a string, but its values can be parsed into all kinds of types. Since this is done by pydantic, everything that works there will work for query parameters as well.

from datetime import datetime, timedelta
from typing import Any, Dict, List

from starlite import Starlite, get


@get("/")
def index(
    date: datetime,
    number: int,
    floating_number: float,
    strings: List[str],
) -> Dict[str, Any]:
    return {
        "datetime": date + timedelta(days=1),
        "int": number,
        "float": floating_number,
        "list": strings,
    }


app = Starlite(route_handlers=[index])


# run: /?date=2022-11-28T13:22:06.916540&floating_number=0.1&number=42&strings=1&strings=2
from datetime import datetime, timedelta
from typing import Any

from starlite import Starlite, get


@get("/")
def index(
    date: datetime,
    number: int,
    floating_number: float,
    strings: list[str],
) -> dict[str, Any]:
    return {
        "datetime": date + timedelta(days=1),
        "int": number,
        "float": floating_number,
        "list": strings,
    }


app = Starlite(route_handlers=[index])


# run: /?date=2022-11-28T13:22:06.916540&floating_number=0.1&number=42&strings=1&strings=2

Specifying alternative names and constraints#

Sometimes you might want to “remap” query parameters to allow a different name in the URL than what’s being used in the handler function. This can be done by making use of Parameter.

from typing import Dict

from starlite import Parameter, Starlite, get


@get("/")
def index(snake_case: str = Parameter(query="camelCase")) -> Dict[str, str]:
    return {"param": snake_case}


app = Starlite(route_handlers=[index])

# run: /?camelCase=foo
from starlite import Parameter, Starlite, get


@get("/")
def index(snake_case: str = Parameter(query="camelCase")) -> dict[str, str]:
    return {"param": snake_case}


app = Starlite(route_handlers=[index])

# run: /?camelCase=foo

Here, we remap from snake_case in the handler function to camelCase in the URL. This means that for the URL http://127.0.0.1:8000?camelCase=foo , the value of camelCase will be used for the value of the snake_case parameter.

Parameter also allows us to define additional constraints:

from typing import Dict

from starlite import Parameter, Starlite, get


@get("/")
def index(param: int = Parameter(gt=5)) -> Dict[str, int]:
    return {"param": param}


app = Starlite(route_handlers=[index])
from starlite import Parameter, Starlite, get


@get("/")
def index(param: int = Parameter(gt=5)) -> dict[str, int]:
    return {"param": param}


app = Starlite(route_handlers=[index])

In this case, param is validated to be an integer larger than 5.

The Parameter Function#

Parameter is a wrapper on top of the pydantic Field function that extends it with a set of Starlite specific kwargs. As such, you can use most of the kwargs of Field with Parameter and have the same parsing and validation. The additional kwargs accepted by Parameter are passed to the resulting pydantic FieldInfo as an extra dictionary and have no effect on the working of pydantic itself.

Layered Parameters#

As part of Starlite’s “layered” architecture, you can declare parameters not only as part of individual route handler functions, but also on other layers of the application:

from typing import Dict, Union

from starlite import Controller, Parameter, Router, Starlite, get


class MyController(Controller):
    path = "/controller"
    parameters = {
        "controller_param": Parameter(int, lt=100),
    }

    @get("/{path_param:int}")
    def my_handler(
        self,
        path_param: int,
        local_param: str,
        router_param: str,
        controller_param: int = Parameter(int, lt=50),
    ) -> Dict[str, Union[str, int]]:
        return {
            "path_param": path_param,
            "local_param": local_param,
            "router_param": router_param,
            "controller_param": controller_param,
        }


router = Router(
    path="/router",
    route_handlers=[MyController],
    parameters={
        "router_param": Parameter(str, regex="^[a-zA-Z]$", header="MyHeader", required=False),
    },
)

app = Starlite(
    route_handlers=[router],
    parameters={
        "app_param": Parameter(str, cookie="special-cookie"),
    },
)
from typing import Union

from starlite import Controller, Parameter, Router, Starlite, get


class MyController(Controller):
    path = "/controller"
    parameters = {
        "controller_param": Parameter(int, lt=100),
    }

    @get("/{path_param:int}")
    def my_handler(
        self,
        path_param: int,
        local_param: str,
        router_param: str,
        controller_param: int = Parameter(int, lt=50),
    ) -> dict[str, Union[str, int]]:
        return {
            "path_param": path_param,
            "local_param": local_param,
            "router_param": router_param,
            "controller_param": controller_param,
        }


router = Router(
    path="/router",
    route_handlers=[MyController],
    parameters={
        "router_param": Parameter(str, regex="^[a-zA-Z]$", header="MyHeader", required=False),
    },
)

app = Starlite(
    route_handlers=[router],
    parameters={
        "app_param": Parameter(str, cookie="special-cookie"),
    },
)
from starlite import Controller, Parameter, Router, Starlite, get


class MyController(Controller):
    path = "/controller"
    parameters = {
        "controller_param": Parameter(int, lt=100),
    }

    @get("/{path_param:int}")
    def my_handler(
        self,
        path_param: int,
        local_param: str,
        router_param: str,
        controller_param: int = Parameter(int, lt=50),
    ) -> dict[str, str | int]:
        return {
            "path_param": path_param,
            "local_param": local_param,
            "router_param": router_param,
            "controller_param": controller_param,
        }


router = Router(
    path="/router",
    route_handlers=[MyController],
    parameters={
        "router_param": Parameter(str, regex="^[a-zA-Z]$", header="MyHeader", required=False),
    },
)

app = Starlite(
    route_handlers=[router],
    parameters={
        "app_param": Parameter(str, cookie="special-cookie"),
    },
)

In the above we declare parameters on the app, router and controller levels in addition to those declared in the route handler. Let’s look at these closer.

  • app_param is a cookie param with the key special-cookie. We type it as str by passing this as an arg to the Parameter function. This is required for us to get typing in the OpenAPI docs. Additionally, this parameter is assumed to be required because it is not explicitly declared as required=False. This is important because the route handler function does not declare a parameter called app_param at all, but it will still require this param to be sent as part of the request of validation will fail.

  • router_param is a header param with the key MyHeader. Because its declared as required=False , it will not fail validation if not present unless explicitly declared by a route handler - and in this case it is. Thus, it is actually required for the router handler function that declares it as an str and not an Optional[str]. If a string value is provided, it will be tested against the provided regex.

  • controller_param is a query param with the key controller_param. It has an lt=100 defined on the controller, which means the provided value must be less than 100. Yet the route handler re-declares it with an lt=50 , which means for the route handler this value must be less than 50.

  • local_param is a route handler local query parameter, and path_param is a path parameter.

Note

You cannot declare path parameters in different application layers. The reason for this is to ensure simplicity - otherwise parameter resolution becomes very difficult to do correctly.