Read Query Parameters

Problem

A listing page needs the search term, page number, and selected filters from the query string. You want typed values without writing request.GET.get(...) plumbing in every callable.

Solution

Annotate a @context parameter with the DQuery[T] marker. The framework reads request.GET, coerces the value to the annotated type, and injects it. DQuery supports str, int, bool, float, UUID, Decimal, date, datetime, and list[T] for multi-value parameters.

Walkthrough

Read a Single Parameter

Declare the parameter with a type and a default. The default is used when the key is absent from the query string.

storefront/page.py
from catalog.models import Product
from next.pages import context
from next.urls import DQuery

DEFAULT_FEATURED = 3
MAX_FEATURED = 12

@context("featured")
def featured(show: DQuery[int] = DEFAULT_FEATURED) -> list[Product]:
    count = max(1, min(MAX_FEATURED, show if isinstance(show, int) else DEFAULT_FEATURED))
    return list(
        Product.objects.filter(in_stock=True)
        .select_related("category")
        .order_by("-created_at")[:count],
    )

A request to /?show=8 injects show=8 as an int. A request to / injects the default 3.

Type Coercion

The annotation drives coercion of the raw query string.

Note

The marker only coerces the type. Clamp or otherwise bound the value yourself, as the featured example does with max and min.

DQuery[int].

Parsed with int(). A value that does not parse, such as ?show=abc, falls back to the raw string rather than raising. Validate when a bad value must be rejected.

DQuery[float].

Parsed with float(), with the same string fallback on a parse failure.

DQuery[bool].

True when the value is 1, true, or yes, case-insensitive. Every other value, including 0 and false, is False.

DQuery[UUID].

Parsed with UUID(...). A value that does not parse falls back to the raw string.

DQuery[Decimal].

Parsed with Decimal(...). A value that does not parse falls back to the raw string.

DQuery[date] and DQuery[datetime].

Parsed via date.fromisoformat and datetime.fromisoformat. A value that does not parse falls back to the raw string.

DQuery[str].

The raw value, unchanged.

DQuery[list[T]].

Three wire formats are accepted, but only one is consulted per request. request.GET.getlist(name) runs first. When it returns more than one value, the plain repeated form ?brand=Acme&brand=Globex is used as-is. When the plain value is empty, the resolver falls back to the bracket form ?brand[]=Acme&brand[]=Globex. When the plain value is a single non-empty string that contains a comma, the resolver falls back to the comma-delimited form ?brand=Acme,Globex. Each element is then coerced using the same rules as the scalar form for T.

A scalar parameter that is absent from the query string receives the declared default, or None when no default is given. A DQuery[list[T]] parameter that is absent in all three wire formats returns the declared default, or an empty list when no default is given.

Read Several Typed Parameters

A single callable can mix scalar and list parameters. Each annotation drives its own coercion.

storefront/catalog/page.py
from next.pages import context
from next.urls import DQuery

@context("results")
def results(
    q: DQuery[str] = "",
    page: DQuery[int] = 1,
    in_stock: DQuery[bool] = False,
    brand: DQuery[list[str]] = (),
) -> dict:
    return {"q": q, "page": page, "in_stock": in_stock, "brands": list(brand)}

List elements follow the same three wire formats described above under Type Coercion.

Build a Typed Snapshot With a Provider

When several callables need the same filter set, parse it once into a frozen dataclass. get_multi_values reads a multi-value parameter through the same three wire formats as DQuery[list[T]].

catalog/providers.py
from dataclasses import dataclass
from next.urls import get_multi_values

@dataclass(frozen=True, slots=True)
class Filters:
    q: str = ""
    brands: tuple[str, ...] = ()
    in_stock: bool = False
    sort: str = "newest"

def parse_filters(request):
    g = request.GET
    return Filters(
        q=g.get("q", "").strip(),
        brands=tuple(get_multi_values(request, "brand")),
        in_stock=g.get("in_stock", "").lower() in {"1", "true", "yes"},
        sort=g.get("sort") or "newest",
    )

Render the Form

Search is idempotent, so the filter form uses method="get" and posts back to the same page. A bookmarked URL reproduces the same listing. Reserve @action for POST side effects such as creating or deleting rows.

storefront/catalog/_cards/filter_panel/component.djx
<form method="get" action="{{ submit_url }}" data-filter-form>
  <input name="q" type="search" value="{{ current_filters.q }}"/>
  {% for brand in all_brands %}
    <label>
      <input type="checkbox" name="brand" value="{{ brand }}"
             {% if brand in current_filters.brands %}checked{% endif %}/>
      {{ brand }}
    </label>
  {% endfor %}
  {% component "button" type="submit" text="Apply filters" variant="default" %}
</form>

Share the Snapshot Across a Layout Chain

Register a resolved value with inherit_context=True so nested pages receive the same instance through DI. The [category] bracket segment becomes a URL kwarg. The callable reads it with DUrl[str] through a parameter named category to match the segment, then resolves it once. Child callables then ask for category by parameter name and never re-query.

storefront/catalog/[category]/page.py
from catalog.models import Category
from django.http import Http404
from next.pages import context
from next.urls import DUrl

@context("category", inherit_context=True)
def category(category: DUrl[str]) -> Category:
    try:
        return Category.objects.get(slug=category)
    except Category.DoesNotExist as exc:
        raise Http404 from exc

A descendant page reads the resolved instance back through its parameter name.

storefront/catalog/[category]/products/page.py
from catalog.models import Category, Product
from next.pages import context

@context("products")
def products(category: Category) -> list[Product]:
    return list(Product.objects.filter(category=category))

Verification

Open the listing page with a faceted query string and confirm the response reflects every parameter.

shell
uv run python manage.py runserver

Visiting /catalog/?q=iphone&brand=Acme&brand=Globex&in_stock=1&page=2 filters by search term, two brands, and stock, on the second page. The bracket form ?brand[]=Acme&brand[]=Globex and the comma form ?brand=Acme,Globex produce the same listing.

See Also

See also

Dependency Injection for the built-in providers. Reverse URLs for building query strings from code. URL Reversing for DUrl on the path versus DQuery in the query string. File Router for bracket segments that become URL kwargs.