Django query parameter validation using Pydantic, inspired by FastAPI
Project description
Django QP
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:
QueryParamsMixinViewmust appear before the view class in the inheritance list. ATypeErroris 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
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
be6e54fbe09502d59f6829c8ef6d24989744838728bd981f506073eb5142d398
|
|
| MD5 |
8601341c4bcbcb97e78cfdf5f225be32
|
|
| BLAKE2b-256 |
834e46f0872c226cec3287541f88f995f7346385b67ad010ecf2db7cb7447c57
|
Provenance
The following attestation bundles were made for django_qp-0.2.0.tar.gz:
Publisher:
publish.yml on dfm88/django_qp
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
django_qp-0.2.0.tar.gz -
Subject digest:
be6e54fbe09502d59f6829c8ef6d24989744838728bd981f506073eb5142d398 - Sigstore transparency entry: 1193689894
- Sigstore integration time:
-
Permalink:
dfm88/django_qp@a1e9d8a32b24c528abea256fac5eac284a6058ed -
Branch / Tag:
refs/heads/main - Owner: https://github.com/dfm88
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@a1e9d8a32b24c528abea256fac5eac284a6058ed -
Trigger Event:
workflow_dispatch
-
Statement type:
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
63ccbc2ea66d5f15af7d82aa74f8d34ad132a3275934b178cbeead46d6377c5a
|
|
| MD5 |
7521040061e65869dbf8a67ae4aa71a9
|
|
| BLAKE2b-256 |
37bd823a0e75a54ad64b38d09a1d41c48d2cf4a32484730f543e170cc6b09051
|
Provenance
The following attestation bundles were made for django_qp-0.2.0-py3-none-any.whl:
Publisher:
publish.yml on dfm88/django_qp
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
django_qp-0.2.0-py3-none-any.whl -
Subject digest:
63ccbc2ea66d5f15af7d82aa74f8d34ad132a3275934b178cbeead46d6377c5a - Sigstore transparency entry: 1193689935
- Sigstore integration time:
-
Permalink:
dfm88/django_qp@a1e9d8a32b24c528abea256fac5eac284a6058ed -
Branch / Tag:
refs/heads/main - Owner: https://github.com/dfm88
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@a1e9d8a32b24c528abea256fac5eac284a6058ed -
Trigger Event:
workflow_dispatch
-
Statement type: