Query parameters

Parameters not included in the Path are automatically treated as Query parameters.

  • Required: Declared as function arguments without default values.
  • Optional: Declared with default values.
import flask
import flask_typed_routes as ftr

app = flask.Flask(__name__)
ftr.FlaskTypedRoutes(app=app)


@app.route('/items/')
def get_items(needy: str, skip: int = 0, limit: int = 100):
    data = {
        'needy': needy,
        'skip': skip,
        'limit': limit,
    }
    return flask.jsonify(data)

Validation:

  • needy: Required and must be a string.
  • skip: Optional and must be an integer. If not included, it defaults to 0.
  • limit: Optional and must be an integer. If not included, it defaults to 100.

Valid request: GET http://127.0.0.1:5000/items/?needy=passed&skip=20

{
  "limit": 100,
  "needy": "passed",
  "skip": 20
}

Bad request: If needy is not included in the request http://127.0.0.1:5000/items/

{
  "errors": [
    {
      "input": {},
      "loc": [
        "query",
        "needy"
      ],
      "msg": "Field required",
      "type": "missing",
      "url": "https://errors.pydantic.dev/2.9/v/missing"
    }
  ]
}

Custom validations

You can apply additional validation using Pydantic's custom types with constraints, or define your own custom data types

import typing as t

import annotated_types as at
import flask
import pydantic

import flask_typed_routes as ftr

app = flask.Flask(__name__)
ftr.FlaskTypedRoutes(app=app)

# Custom Types for additional validation combining Pydantic and Annotated
Needy = t.Annotated[str, at.MinLen(3), at.MaxLen(10)]
Limit = t.Annotated[int, at.Ge(1), at.Le(100), pydantic.Field(alias="size")]


@app.route('/items/')
def get_items(needy: Needy, skip: int = 0, limit: Limit = 100):
    data = {
        'needy': needy,
        'skip': skip,
        'limit': limit,
    }
    return flask.jsonify(data)

Alternatively, you can use the Query field. This field is an extension of Pydantic's field, offering powerful validation capabilities. This flexibility allows you to tailor query parameter validation to your application's specific needs.

Tip

The Query field is supported aliasing. You can use the alias argument to define the query parameter name in the request.

import typing as t

import flask
import flask_typed_routes as ftr

app = flask.Flask(__name__)
ftr.FlaskTypedRoutes(app=app)

Needy = t.Annotated[str, ftr.Query(min_length=3, max_length=10)]
Limit = t.Annotated[int, ftr.Query(ge=1, le=100, alias="size")]


@app.route('/items/')
def get_items(needy: Needy, skip: int = 0, limit: Limit = 100):
    data = {
        'needy': needy,
        'skip': skip,
        'limit': limit,
    }
    return flask.jsonify(data)

Validation:

  • needy: Required and must be a string between 3 and 10 characters.
  • skip: Optional and must be an integer.
  • limit: Optional and must be an integer between 1 and 100, and must be named size in the request.

Example request: GET http://127.0.0.1:5000/items/?needy=passed&size=20

{
  "limit": 20,
  "needy": "passed",
  "skip": 0
}

Pydantic models

If you have a group of query parameters that are related, you can create a Pydantic model to declare them.

This would allow you to re-use the model in multiple places and also to declare validations and metadata for all the parameters at once.

import typing as t

import pydantic
import flask
import flask_typed_routes as ftr

app = flask.Flask(__name__)
ftr.FlaskTypedRoutes(app=app)


class QueryParams(pydantic.BaseModel):
    status: bool
    skip: int = 0
    limit: int = 10
    tracking_number: t.Annotated[int, pydantic.Field(alias="tracking", le=3)] = 1
    payment_method: t.Literal["cash", "credit"] = "credit"


@app.get('/orders/<user_id>/')
def get_orders(
    user_id: int,
    params: t.Annotated[QueryParams, ftr.Query()]
):
    data = {'user_id': user_id, "params": params.model_dump()}
    return flask.jsonify(data)

Warning

The Query field can only be directly specified in the function signature. When using Pydantic models, you must use Pydantic's fields.

Example request: GET ttp://127.0.0.1:5000/orders/233/?status=true

{
  "params": {
    "limit": 10,
    "payment_method": "credit",
    "skip": 0,
    "status": true,
    "tracking_number": 1
  },
  "user_id": 233
}

Parsing Arrays

If you want to allow a query parameter to parse as an Array, you can use set, tuple, or list annotations.

Tip

You can use the set type hint to validate that the values are unique.

import typing as t

import flask
import flask_typed_routes as ftr
import pydantic

app = flask.Flask(__name__)
ftr.FlaskTypedRoutes(app=app)

Tags = t.Annotated[list[str], pydantic.Field(alias="tag")]


@app.get('/users/<user_id>/')
def get_users(user_id: int, tags: Tags = ()):
    data = {'user_id': user_id, "tags": tags}
    return flask.jsonify(data)

Example request: GET http://127.0.0.1:5000/users/123/?tag=hello&tag=world

{
  "tags": [
    "hello",
    "world"
  ],
  "user_id": 123
}

Note

It is important to highlight that the previous URL contains multiple query parameters named tag.

Tip

If the URL includes a query parameter with multiple values separated by commas, pipes(|), or spaces, the resulting list will contain a single element with the entire string.

To retrieve each value separately, you need to set the explode parameter in the Query field and specify the style parameter to define the serialization format.

Here’s an example of how to do it:

import typing as t

import flask
import flask_typed_routes as ftr

app = flask.Flask(__name__)
ftr.FlaskTypedRoutes(app=app)

# By default, the 'style' is 'form', which means that the values are separated by commas.
TagsByComma = t.Annotated[list[str], ftr.Query(explode=False)]

# You can also use 'pipeDelimited' or 'spaceDelimited' as the 'style' to indicate another serialization style delimiter.
TagsBySpace = t.Annotated[list[str], ftr.Query(explode=False, style="spaceDelimited")]
TagsByPipe = t.Annotated[list[str], ftr.Query(explode=False, style="pipeDelimited")]


@app.get('/tags/comma/')
def get_tags_by_comma(tags: TagsByComma = ()):
    return flask.jsonify({"tags": tags})


@app.get('/tags/space/')
def get_tags_by_space(tags: TagsBySpace = ()):
    return flask.jsonify({"tags": tags})


@app.get('/tags/pipe/')
def get_tags_by_pipe(tags: TagsByPipe = ()):
    return flask.jsonify({"tags": tags})

Example requests:

  • By commas: http://localhost:5000/tags/comma/?tags=hello,world
  • By spaces: http://localhost:5000/tags/space/?tags=hello world
  • By pipes: http://localhost:5000/tags/pipe/?tags=hello|world

You will see the JSON response as:

{
  "tags": [
    "hello",
    "world"
  ]
}

Parsing Objects

Query parameters can be parsed as Objects using dictionaries or Pydantic models. The library follows the form style and of OpenAPI parameter serialization for objects.

The query parameters can be serialized in different ways using the style and explode parameters.

style explode URL
form (default) true (default) /users?role=admin&first_name=Alex
form false /users?info=role,admin,first_name,Alex
pipeDelimited true/false n/a
pipeDelimited true/false n/a

Parsing a single query parameter as an object

The explode parameter should be set to False to parse a single query parameter as an object using the form style.

Warning

The supported styles are form, spaceDelimited, and pipeDelimited, but only form is supported for objects.

Using a dictionary

import typing as t

import flask

import flask_typed_routes as ftr

app = flask.Flask(__name__)
ftr.FlaskTypedRoutes(app=app)


@app.get('/users/')
def get_users(info: t.Annotated[dict, ftr.Query(explode=False)]):
    return flask.jsonify({'info': info})

Example request: GET http://127.0.0.1:5000/users/?info=role,admin,first_name,Alex

{
  "info": {
    "first_name": "Alex",
    "role": "admin"
  }
}

Using Pydantic models:

import typing as t

import flask
import pydantic

import flask_typed_routes as ftr

app = flask.Flask(__name__)
ftr.FlaskTypedRoutes(app=app)


class UserInfo(pydantic.BaseModel):
    role: str
    first_name: str


class QueryParams(pydantic.BaseModel):
    info: UserInfo
    status: str = "default"


@app.get('/users/')
def get_users(params: t.Annotated[QueryParams, ftr.Query(explode=False)]):
    return flask.jsonify(params.model_dump())

Example request: GET http://127.0.0.1:5000/users/?info=role,admin,first_name,Alex

{
  "info": {
    "first_name": "Alex",
    "role": "admin"
  },
  "status": "default"
}

Parsing JSONs

You can parse JSON data from the query parameters using the Json type wrapper of Pydantic.

import typing as t

import flask
import pydantic

import flask_typed_routes as ftr

app = flask.Flask(__name__)
ftr.FlaskTypedRoutes(app=app)


class UserInfo(pydantic.BaseModel):
    role: str
    first_name: str


class QueryParams(pydantic.BaseModel):
    info: pydantic.Json[UserInfo]


@app.get('/users/signature/')
def get_users1(info: pydantic.Json[UserInfo]):
    # Using directly the `Json` type in the function signature
    return flask.jsonify(info.model_dump())


@app.get('/users/model/')
def get_users2(query: t.Annotated[QueryParams, ftr.Query()]):
    # Using a Pydantic model with the `Json` type
    return flask.jsonify(query.model_dump()['info'])

Example request:

  • GET http://127.0.0.1:5000/users/signature/?info={"role": "admin", "first_name": "Alex"}
  • GET http://127.0.0.1:5000/users/model/?info={"role": "admin", "first_name": "Alex"}

The response will be:

{
  "first_name": "Alex",
  "role": "admin"
}