djangorestframework-stubs icon indicating copy to clipboard operation
djangorestframework-stubs copied to clipboard

Improve GenericAPIView Type Hints for Non-Model Serializers

Open pablo-snz opened this issue 9 months ago • 1 comments

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 as BaseSerializer[Any].

Steps to Reproduce

  1. 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)
  1. 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 to BaseSerializer[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.

pablo-snz avatar Mar 18 '25 21:03 pablo-snz

pr is welcome :)

sobolevn avatar Mar 19 '25 02:03 sobolevn