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:
The path parameter is defined in the
@get
decorator, which declares both the parameter’s nameuser_id
) and typeint
.The decorated function
get_user
defines a parameter with the same name as the parameter defined in thepath
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 keyspecial-cookie
. We type it asstr
by passing this as an arg to theParameter
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 asrequired=False
. This is important because the route handler function does not declare a parameter calledapp_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 keyMyHeader
. Because its declared asrequired=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 anstr
and not anOptional[str]
. If a string value is provided, it will be tested against the provided regex.controller_param
is a query param with the keycontroller_param
. It has anlt=100
defined on the controller, which means the provided value must be less than 100. Yet the route handler re-declares it with anlt=50
, which means for the route handler this value must be less than 50.local_param
is a route handler local query parameter, andpath_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.