-
-
Notifications
You must be signed in to change notification settings - Fork 128
Description
Description
Currently, the DRF stubs for GenericAPIView
tie the serializer type to a model-based type variable. This approach works fine for model serializers but causes issues for non-model-based serializers that add custom methods (e.g., a to_dto()
method). For example, when using a custom serializer that extends BaseSerializer[Any]
with additional functionality, mypy complains that these methods do not exist.
The current stubs define the serializer class attribute as:
serializer_class: type[BaseSerializer[_MT_co]] | None
and the return type of get_serializer()
as:
def get_serializer(self, *args: Any, **kwargs: Any) -> BaseSerializer[_MT_co]: ...
with _MT_co
being a covariant TypeVar
bounded to django.db.models.Model
. This causes two problems:
- The generic type parameter is forced to be a subtype of
Model
, which doesn’t fit non-model serializers. - Custom serializer methods (such as
to_dto
) are not recognized because the type is inferred asBaseSerializer[Any]
.
Steps to Reproduce
- Create a custom serializer that extends
BaseSerializer[Any]
with an additional method:
from rest_framework.serializers import BaseSerializer
from rest_framework.generics import GenericAPIView
from rest_framework.response import Response
from rest_framework.request import Request
class MySerializer(BaseSerializer[Any]):
def to_dto(self) -> dict:
return {"data": "example"}
def to_dto(self, instance: Any) -> Any:
return instance
class MyView(GenericAPIView):
serializer_class = MySerializer
def post(self, request: Request) -> Response:
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
# Mypy error: "BaseSerializer[Any]" has no attribute "to_dto"
data = serializer.to_dto()
return Response(data)
- Run mypy. You will see an error indicating that the return type of
get_serializer()
does not have the custom to_dto attribute.
Expected Behavior
Mypy should infer the correct serializer type from the serializer_class
attribute so that methods defined in custom serializers (like to_dto
) are recognized. In other words, get_serializer()
should return an instance of the actual serializer type instead of defaulting to BaseSerializer[Any]
.
Proposed Solution
Introduce a second generic type variable for the serializer and update the stubs to decouple the serializer type from the model type. For example, define a new type variable _S
with a default value and bound to BaseSerializer[Any]
while considering covariance for the model type:
from typing import Any, Generic, TypeVar
from django.db.models import Model
from rest_framework.serializers import BaseSerializer
from rest_framework import views
_MT_co = TypeVar("_MT_co", bound=Model, covariant=True)
_S = TypeVar("_S", bound=BaseSerializer[Any], default=BaseSerializer[Any]) # New serializer type variable
class GenericAPIView(views.APIView, UsesQuerySet[_MT_co], Generic[_MT_co, _S]):
queryset: "QuerySet[_MT_co] | Manager[_MT_co] | None"
serializer_class: type[_S] | None
lookup_field: str
lookup_url_kwarg: str | None
filter_backends: Sequence[type[BaseFilterBackend | BaseFilterProtocol[_MT_co]]]
pagination_class: type[BasePagination] | None
def __class_getitem__(cls, *args: Any, **kwargs: Any) -> type[Self]: ...
def get_object(self) -> _MT_co: ...
def get_serializer(self, *args: Any, **kwargs: Any) -> _S: ...
def get_serializer_class(self) -> type[_S]: ...
def get_serializer_context(self) -> dict[str, Any]: ...
def filter_queryset(self, queryset: QuerySet[_MT_co]) -> QuerySet[_MT_co]: ...
@property
def paginator(self) -> BasePagination | None: ...
def paginate_queryset(self, queryset: QuerySet[_MT_co] | Sequence[Any]) -> Sequence[Any] | None: ...
def get_paginated_response(self, data: Any) -> Response: ...
This change maintains backward compatibility:
- For model-based serializers, users can continue not specifying the second type parameter, and
_S
will default toBaseSerializer[Any]
. - For custom serializers with additional methods, users can explicitly set the type, and mypy will infer the correct return type from
get_serializer()
, thereby recognizing the extra methods.