Skip to main content

Django query parameter validation using Pydantic, inspired by FastAPI

Project description

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...

Now, with django-qp, you can harness the power of Pydantic validation:

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})

Django QP

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 >= 3.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

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.

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.1.0.tar.gz (91.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.1.0-py3-none-any.whl (12.5 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: django_qp-0.1.0.tar.gz
  • Upload date:
  • Size: 91.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.2 {"installer":{"name":"uv","version":"0.11.2","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for django_qp-0.1.0.tar.gz
Algorithm Hash digest
SHA256 64f263ab1ab99423ddf42b0319c9439c9aee81e54c4ef62e22794aebdd6d455e
MD5 987f42df47f7b70109c91acef4f796b2
BLAKE2b-256 56665382831bb89bd4294bf5a50a13e06ac6bf55d732572abdd9e6ec97c272ff

See more details on using hashes here.

File details

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

File metadata

  • Download URL: django_qp-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 12.5 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.2 {"installer":{"name":"uv","version":"0.11.2","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for django_qp-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 765adc1ef5ab5a0dcb3660362ba819d5bf773d2334a71e955de978cf1c4b423d
MD5 a36ad6c51aa23cb2407dd62c486b4fbb
BLAKE2b-256 c6b52e3a43ab2d2fa45db522d855fd9064d8e3fecc3b32a5a5be509f82dcb735

See more details on using hashes here.

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