Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Swagger ui breaks when using method mapping or extend_schema for mapped method #1373

Open
zamoosh opened this issue Jan 30, 2025 · 3 comments
Labels
bug Something isn't working low priority issues that would be nice to fix but have low impact

Comments

@zamoosh
Copy link

zamoosh commented Jan 30, 2025

Describe the bug
When I try to use extend_schema for an action which has a mapping method, swagger ui will break and returns this error:

AssertionError: Incompatible AutoSchema used on View <class 'client.admin.user.user_api.UserApi'>. Is DRF's DEFAULT_SCHEMA_CLASS pointing to "drf_spectacular.openapi.AutoSchema" or any other drf-spectacular compatible AutoSchema?


To Reproduce
For a configured ModelViewSet api, use @extend_schema_view and add a mapping method for an action which has extend_schema.


Expected behavior
It would be really nice if the error describes what to do...


Here is sample code:

user_api.py

SWAGGER_TAGS = ["User"]



@extend_schema(
    tags=SWAGGER_TAGS,
)
@extend_schema_view(
    list=...
    # TODO: the schema for response must be implemented
    personalization=extend_schema(
        request=WriteUpdatePersonalizationSerializerAdmin,
    ),
)
class UserApi(ModelViewSet):
    permission_classes = [IsAdminUser]
    lookup_field = "id"
    lookup_url_kwarg = "id"
    serializer_class = WriteUserSerializer
    filter_backends = [CustomFilterBackend, CustomOrderingFilter, SearchFilter]
    filterset_class = UserFilter
    search_fields = ["cellphone", "full_name", "email"]
    ordering_fields = ["cellphone", "first_name", "last_name", "email"]
    ordering = ["-id"]
    pagination_class = CustomPagination
    queryset = User.objects.filter(deleted_at=None).annotate(
        full_name=Concat(F("first_name"), Value(" "), F("last_name"))
    )

    def get_serializer_class(self) -> serializers.Serializer:
        serializer = super(UserApi, self).get_serializer_class()
        match self.action:
            case "list" | "retrieve":
                serializer = ReadUserSerializer
            case "personalization":
                serializer = WriteUpdatePersonalizationSerializerAdmin
        return serializer

    @action(methods=["put"], detail=False, url_path="personalization")
    def personalization(self, request, *args, **kwargs):
        instance: User = request.user
        serializer = self.get_serializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        serializer.update(instance, serializer.validated_data)

        context = {
            "msg": "با موفقیت بروزرسانی شد",
            "personalization": instance.personalization,
        }
        return Response(context, status.HTTP_200_OK)

    @personalization.mapping.get
    def get_personalization(self, request, *args, **kwargs):
        context = {
            "personalization": request.user.personalization,
        }
        return Response(context, status.HTTP_200_OK)

And here is my serializser:

class WriteUpdatePersonalizationSerializerAdmin(serializers.Serializer):
    key = serializers.CharField(trim_whitespace=True)
    value = serializers.JSONField(allow_null=True, required=False)
    upsert = serializers.BooleanField(default=True)

    def to_representation(self, instance: upsert):
        return instance


    # TODO: need to be done later
    def update(self, instance: User, validated_data: dict):
        try:
            if validated_data["upsert"] is True:
                instance.personalization[validated_data["key"]] = validated_data["value"]
            else:
                pass
            instance.save(update_fields=["personalization"])
            return instance
        except Exception as e:
            raise CustomExceptionOnlyMsg(str(e), status.HTTP_400_BAD_REQUEST)

Here is my settings.py:

# ...

REST_FRAMEWORK = {
    "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
    "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.LimitOffsetPagination",
    "PAGE_SIZE": 20,
    "DEFAULT_PERMISSION_CLASSES": ("rest_framework.permissions.IsAuthenticated",),
    "DEFAULT_AUTHENTICATION_CLASSES": [
        "rest_framework_simplejwt.authentication.JWTAuthentication",
    ],
    "DEFAULT_FILTER_BACKENDS": ("django_filters.rest_framework.DjangoFilterBackend",),
}

# ...

SPECTACULAR_SETTINGS = {
    "TITLE": "...",
    "DESCRIPTION": "",
    "VERSION": "0.0.0",
    "SERVE_INCLUDE_SCHEMA": False,
    "COMPONENT_SPLIT_REQUEST": True,
    # "SERVE_PERMISSIONS": ["rest_framework.permissions.IsAuthenticated"],
    "SERVE_PERMISSIONS": ["rest_framework.permissions.AllowAny"],
    # OTHER SETTINGS
}

# ...

How error disappears?

You need to comment out extend_schema for personalization in @extend_schema_view

OR

Comment the mapped method, I mean below function:

@personalization.mapping.get
def get_personalization(self, request, *args, **kwargs):
    context = {
        "personalization": request.user.personalization,
    }
    return Response(context, status.HTTP_200_OK)

and the swagger ui loads with no error...

BTW, thanks alot @tfranzel for the package! 🙏

@UCQualquer
Copy link

I have the same issue. I reverse engineered back to get_view_method_names() not handling methods registered through mapping.*, and this cascades to the assert that raises the message to compare the ExtendedSchema type itself to AutoSchema, instead of an instance of it.

I made a makeshift fix that causes the get_view_method_names() method to register the method:

class MyViewSet(viewsets.ModelViewSet):
    @action(['GET'], True, 'customization')
    def get_customization(self, request, pk: int): ...

    @get_customization.mapping.post
    def set_customization(self, request, pk: int): ...

    set_customization.mapping = None # Create a "mapping" attribute: https://github.com/tfranzel/drf-spectacular/blob/5734744545bd0c4801c4c010d13edc77328fd412/drf_spectacular/drainage.py#L187

This has been an issue since 2022.

@tfranzel
Copy link
Owner

Hi, so looks like a corner case in internal initialization. Unfortunately, you hit the one combination that is not properly dealt with. The others are already covered by tests.

@action decorated @*.mapping.* decorated Result
case 1 No No
case 2 Yes No
case 3 No Yes
case 4 Yes Yes

HOTFIX: For now, just add a empty decorator to circumvent this edge case. It should be fixed, but I deem this low priority at the moment.

    @extend_schema_view(
        list=extend_schema(responses=ZSerializer),
    )
    class XViewset(mixins.ListModelMixin, viewsets.GenericViewSet):
        ...

        @extend_schema(responses=XSerializer)
        @action(methods=["put"], detail=False, url_path="personalization")
        def personalization(self, request, *args, **kwargs):
            pass

        @extend_schema() #  <---- this is the important part; may or may not contain parameters.
        @personalization.mapping.get
        def get_personalization(self, request, *args, **kwargs):
            pass

This has been an issue since 2022.

@UCQualquer, I was not aware of that problem and to my knowledge nobody ever opened an issue for this here. I'm not actively looking for issues on SO.

@tfranzel tfranzel added bug Something isn't working low priority issues that would be nice to fix but have low impact labels Feb 12, 2025
@UCQualquer
Copy link

I was not aware of that problem and to my knowledge nobody ever opened an issue for this here. I'm not actively looking for issues on SO.

No problem. I too could only find that SO question after some hours digging on the issue, and doubt many have found this issue too.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working low priority issues that would be nice to fix but have low impact
Projects
None yet
Development

No branches or pull requests

3 participants