Skip to main content

Django query parameter validation using Pydantic, inspired by FastAPI

Project description

Django QP

CI PyPI Python Django License: MIT

Are you tired of accessing Django query parameters like this?

# Traditional Django view
class MyView(View):
    def get(self, request):
        name = request.GET.get("name")
        age = int(request.GET.get("age", 0))
        # ...manual validation, type conversion, error handling...

Or maybe you're using DRF and validating query parameters with a serializer defined as an inner class of your view, only to access the values through a dict with no IDE autocompletion?

# DRF approach — no IDE autocompletion, dict-based access
class MyView(APIView):
    class QuerySerializer(serializers.Serializer):
        name = serializers.CharField()
        age = serializers.IntegerField(min_value=0)

    def get(self, request):
        serializer = self.QuerySerializer(data=request.query_params)
        serializer.is_valid(raise_exception=True)
        name = serializer.validated_data["name"]  # No autocompletion, always a dict lookup
        age = serializer.validated_data["age"]     # Easy to typo the key, no type safety

With django-qp, you get the power of Pydantic validation with full IDE autocompletion:

from pydantic import BaseModel, Field
from django_qp import QueryParamsMixinView

class UserParams(BaseModel):
    name: str
    age: int = Field(ge=0)

# Use generic type parameter for IDE autocompletion
class MyView(QueryParamsMixinView[UserParams], View):
    validated_params_model = UserParams

    def get(self, request):
        params = self.validated_params  # Validated Pydantic model with IDE autocompletion!
        return JsonResponse({"name": params.name, "age": params.age})

A lightweight library for Django and Django Rest Framework that enables validation of query parameters using Pydantic models, inspired by FastAPI's approach.

Requirements

  • Python >= 3.10, < 3.15
  • Django >= 4.2, < 7.0
  • Pydantic >= 2.0
  • Django REST Framework >= 3.15.2 (optional)

Why use this library?

  • No more manual parsing or type conversion: Query parameters are validated and converted to Python types automatically.
  • Clear, maintainable code: Use Pydantic models to define your expected query parameters.
  • Consistent error handling: Get structured error responses for invalid input.
  • Works with Django, DRF, class-based and function-based views.
  • Supports action-specific validation for ViewSets.
  • Supports method-specific validation for function-based views.
  • Customizable error messages and status codes.
  • Type hints for IDE autocompletion: Get full IntelliSense/autocomplete in your IDE.

Features

  • Validate query parameters using Pydantic models
  • Type conversion and validation
  • Support for Django views and DRF (APIView, ViewSet)
  • Class-based mixins and function-based decorators
  • Support for comma-separated list parameters
  • Action-specific models for ViewSets
  • Method-specific models for function-based views
  • Custom error messages and status codes
  • Generic type annotations for IDE autocompletion
  • Enhanced type hints for request objects in function-based views
  • Transparent async view support (Django 4.2+, DRF 3.15+)

Installation

pip install django-qp

With DRF support:

pip install django-qp[drf]

Or with uv:

uv pip install django-qp        # Django only
uv pip install django-qp[drf]   # with DRF support

Usage

1. Define your Pydantic model

from pydantic import BaseModel, Field

class ProductFilterParams(BaseModel):
    category: str | None = None
    min_price: float = Field(default=0, ge=0)
    max_price: float | None = None
    in_stock: bool = False
    tags: list[str] | None = None  # Will parse comma-separated values

List parameters: Fields typed as list[...] support two patterns that can be combined:

  • Comma-separated: ?tags=python,django["python", "django"]
  • Repeated keys: ?tags=python&tags=django["python", "django"]
  • Combined: ?tags=python,django&tags=fastapi["python", "django", "fastapi"]

2. Use with Django class-based views

from django.views import View
from django.http import JsonResponse
from django_qp import QueryParamsMixinView

# Specify the model as a generic parameter for IDE autocompletion
class ProductListView(QueryParamsMixinView[ProductFilterParams], View):
    validated_params_model = ProductFilterParams

    def get(self, request):
        params = self.validated_params  # IDE will now recognize type as ProductFilterParams
        return JsonResponse({
            "category": params.category,
            "min_price": params.min_price
        })

Important: QueryParamsMixinView must appear before the view class in the inheritance list. A TypeError is raised at class definition time if the order is wrong.

# Correct
class MyView(QueryParamsMixinView[MyParams], View): ...

# Wrong — raises TypeError
class MyView(View, QueryParamsMixinView[MyParams]): ...

3. Use with Django Rest Framework views

Requires pip install django-qp[drf].

from rest_framework.views import APIView
from rest_framework.response import Response
from django_qp import QueryParamsMixinView

class ProductAPIView(QueryParamsMixinView[ProductFilterParams], APIView):
    validated_params_model = ProductFilterParams

    def get(self, request):
        params = self.validated_params
        return Response({
            "result": "success",
            "tags": params.tags
        })

4. Use with ViewSets for action-specific validation

Override get_query_params_class to return different models per action (similar to DRF's get_serializer_class):

from rest_framework.viewsets import ModelViewSet
from rest_framework.decorators import action
from rest_framework.response import Response
from django_qp import QueryParamsMixinView
from typing import cast

class ProductViewSet(QueryParamsMixinView[ProductFilterParams], ModelViewSet):
    queryset = Product.objects.all()
    serializer_class = ProductSerializer

    def get_query_params_class(self, action):
        if action == "list":
            return ProductFilterParams
        elif action == "export":
            return ExportParams
        return None  # No validation for other actions

    def list(self, request):
        params = cast(ProductFilterParams, self.validated_params)
        filtered_products = self.queryset.filter(
            category=params.category,
            price__gte=params.min_price
        )
        # ...

    @action(detail=False, methods=["get"])
    def export(self, request):
        params = cast(ExportParams, self.validated_params)
        return Response({
            "export_format": params.format,
            "with_details": params.include_details
        })

5. Use with function-based views

Simple validation with a single model

from django.http import JsonResponse
from django_qp import validate_query_params, EnhancedHttpRequest

@validate_query_params(ProductFilterParams)
def product_list(request: EnhancedHttpRequest[ProductFilterParams]):
    params = request.validated_params  # IDE autocomplete works here!
    return JsonResponse({"category": params.category})

Method-specific validation

Provide different validation models for different HTTP methods using a dict with lowercase method names:

from django.http import JsonResponse, HttpRequest
from django_qp import validate_query_params

class GetParams(BaseModel):
    filter: str
    sort_by: str | None = None

class PostParams(BaseModel):
    data: str
    priority: int = Field(ge=1, le=5)

# Dictionary mapping HTTP methods to models
@validate_query_params({
    "get": GetParams,
    "post": PostParams,
    "": DefaultParams,  # Optional fallback for unmapped methods
})
def products_api(request: HttpRequest):
    params = request.validated_params

    if request.method == "GET":
        params: GetParams
        return JsonResponse({
            "filter": params.filter,
            "sort_by": params.sort_by
        })
    elif request.method == "POST":
        params: PostParams
        return JsonResponse({
            "data": params.data,
            "priority": params.priority
        })

Dynamic model selection

For more complex scenarios, use a resolver function:

from django.http import JsonResponse
from django_qp import validate_query_params, EnhancedHttpRequest

def get_model_for_request(request):
    if request.GET.get("export") == "true":
        return ExportParams
    return StandardParams

@validate_query_params(get_model_for_request)
def dynamic_api(request: EnhancedHttpRequest[ExportParams | StandardParams]):
    params = request.validated_params

    if hasattr(params, "format"):
        return JsonResponse({"format": params.format})
    else:
        return JsonResponse({"query": params.query})

Customizing Error Messages and Status Codes

You can provide custom error messages and status codes for specific fields and error types:

class CustomView(QueryParamsMixinView[ProductFilterParams], APIView):
    validated_params_model = ProductFilterParams
    field_error_messages = {
        "min_price": {
            "greater_than_equal": "Minimum price must be non-negative.",
            "__all__": "Invalid price provided."
        },
        "category": {
            "type_error": "Category must be a string."
        }
    }
    field_error_status_codes = {
        "min_price": 400,
        "category": 404,
    }

Advanced: Direct parameter validation

from django_qp import process_query_params, QueryParamsError

def custom_view(request):
    try:
        params = cast(
            ProductFilterParams,
            process_query_params(request, ProductFilterParams)
        )
    except QueryParamsError as e:
        return JsonResponse({"error": e.detail}, status=400)
    # ...

Type Hints for Function-Based Views

For better IDE support and type checking in function-based views, use the EnhancedHttpRequest generic type:

from django_qp import validate_query_params, EnhancedHttpRequest
from pydantic import BaseModel

class UserParams(BaseModel):
    name: str
    age: int

@validate_query_params(UserParams)
def my_view(request: EnhancedHttpRequest[UserParams]):
    # Your IDE now knows that:
    params = request.validated_params  # This is a UserParams instance
    name = params.name  # This is a string
    age = params.age    # This is an int

    return HttpResponse(f"Hello {name}, you are {age} years old")

Handling Dynamic Models with Type Casting

When working with dynamic models (such as when using a resolver function), you can use Python's cast() function for better type safety:

from typing import cast
from django_qp import validate_query_params, EnhancedHttpRequest

# Define a resolver function that returns different models
def get_model_for_request(request):
    if request.GET.get("use_detailed") == "true":
        return DetailedParams
    return SimpleParams

@validate_query_params(get_model_for_request)
def dynamic_view(request: EnhancedHttpRequest[SimpleParams | DetailedParams]):
    params = request.validated_params

    # Option 1: Type narrowing with isinstance
    if isinstance(params, DetailedParams):
        # IDE knows this is DetailedParams
        return JsonResponse({"details": params.details})

    # Option 2: Type narrowing with attribute check + cast
    if hasattr(params, "details"):
        # Use cast to tell the type checker what type this is
        detailed_params = cast(DetailedParams, params)
        return JsonResponse({"details": detailed_params.details})

    # Now IDE knows this must be SimpleParams
    return JsonResponse({"name": params.name})

This approach gives you the full benefits of type checking while still supporting dynamic model selection.

Async Views

Async views are supported out of the box. The library auto-detects whether your view is async and handles it transparently — no API changes, no special imports. Both the mixin and decorator work with async views:

# Async CBV — works exactly like sync
class MyView(QueryParamsMixinView[MyParams], View):
    validated_params_model = MyParams

    async def get(self, request):
        params = self.validated_params
        return JsonResponse({"name": params.name})

# Async FBV — works exactly like sync
@validate_query_params(MyParams)
async def my_view(request: EnhancedHttpRequest[MyParams]):
    params = request.validated_params
    return JsonResponse({"name": params.name})

Requires Django >= 4.2. DRF async views (>= 3.15) are also supported.

Running Tests

uv run pytest

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

django_qp-0.2.0.tar.gz (68.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

django_qp-0.2.0-py3-none-any.whl (14.2 kB view details)

Uploaded Python 3

File details

Details for the file django_qp-0.2.0.tar.gz.

File metadata

  • Download URL: django_qp-0.2.0.tar.gz
  • Upload date:
  • Size: 68.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for django_qp-0.2.0.tar.gz
Algorithm Hash digest
SHA256 be6e54fbe09502d59f6829c8ef6d24989744838728bd981f506073eb5142d398
MD5 8601341c4bcbcb97e78cfdf5f225be32
BLAKE2b-256 834e46f0872c226cec3287541f88f995f7346385b67ad010ecf2db7cb7447c57

See more details on using hashes here.

Provenance

The following attestation bundles were made for django_qp-0.2.0.tar.gz:

Publisher: publish.yml on dfm88/django_qp

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file django_qp-0.2.0-py3-none-any.whl.

File metadata

  • Download URL: django_qp-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 14.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for django_qp-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 63ccbc2ea66d5f15af7d82aa74f8d34ad132a3275934b178cbeead46d6377c5a
MD5 7521040061e65869dbf8a67ae4aa71a9
BLAKE2b-256 37bd823a0e75a54ad64b38d09a1d41c48d2cf4a32484730f543e170cc6b09051

See more details on using hashes here.

Provenance

The following attestation bundles were made for django_qp-0.2.0-py3-none-any.whl:

Publisher: publish.yml on dfm88/django_qp

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page