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

Array Schema from OpenApiSerializerExtension not included in the result #391

Closed
mohannad-musleh opened this issue May 19, 2021 · 10 comments
Closed
Labels
bug Something isn't working fix confirmation pending issue has been fixed and confirmation from issue reporter is pending

Comments

@mohannad-musleh
Copy link

Describe the bug
when using OpenApiSerializerExtension that returns schema component with array element as the root, the component will not be included in the final open API schema json.

To Reproduce

extension implementation

from marshmallow import Schema
from drf_spectacular.extensions import OpenApiSerializerExtension

class MarshmallowSerializerExtension(OpenApiSerializerExtension):
    target_class = Schema
    match_subclasses = True
    
    def get_name(self):
        if inspect.isclass(self.target):
            serializer_name = self.target.__name__
        else:
            serializer_name = type(self.target).__name__

        return serializer_name
        
   def map_serializer(self, auto_schema, direction):
       return { # this will not be included in the final result (in components object)
                "type": "array",
                "items": {
                    "type": "object",
                    "properties": {
                        "e1": {
                            "type": "string",
                            "enum": ["a", "b", "c"]
                        },
                    },
                    "xml": {
                        "name": "item"
                    }
                },
                "xml": {
                    "wrapped": True,
                    "name": "arrayresponse"
                }
            }

ViewSet implementation

note: I'm using OpenApiViewExtension, but the issue same as if I use @extend_schema directly in the ViewSet class implementation

from rest_framework.viewsets import ViewSet
from rest_framework_xml.parsers import XMLParser
from rest_framework_xml.renderers import XMLRenderer

class MyViewSet(ViewSet):
    parser_classes = (XMLParser,)
    renderer_classes = (XMLRenderer,)

    def post(self, request, format=None):
        # ...
        pass

ViewSet open api view extension implementation

from drf_spectacular.utils import extend_schema
from drf_spectacular.extensions import OpenApiViewExtension

class MyViewSetExtension(OpenApiViewExtension):
    target_class = 'my_project.views.MyViewSet'
    
    def view_replacement(self):
        class Fixer(self.target_class):
            @extend_schema(request=RequestSerializer) # RequestSerializer as a class that will be handled by MarshmallowSerializerExtension
            def create(self, request):
                pass

Generated schema

notice the components object is empty

{
    "openapi": "3.0.3",
    "info": {
        "title": "My App",
        "version": "1.0",
        "description": "My App APIs documentation"
    },
    "paths": {
        "/api/v1/my_view": {
            "post": {
                "operationId": "my_view_create",
                "description": "The API description",
                "summary": "The API summary",
                "tags": [
                    "my_view"
                ],
                "responses": {
                    "200": {
                        "description": "No response body"
                    }
                }
            }
        }
    },
    "components": {}
}

Expected behavior
I expect the component to be included in the generated schema

expected schema

{
    "openapi": "3.0.3",
    "info": {
        "title": "My App",
        "version": "1.0",
        "description": "My App APIs documentation"
    },
    "paths": {
        "/api/v1/my_view": {
            "post": {
                "operationId": "my_view_create",
                "description": "The API description",
                "summary": "The API summary",
                "tags": [
                    "my_view"
                ],
                "requestBody": {
                    "content": {
                        "application/xml": {
                            "schema": {
                                "$ref": "#/components/schemas/RequestSchema"
                            }
                        }
                    }
                },
                "responses": {
                    "200": {
                        "description": "No response body"
                    }
                }
            }
        }
    },
    "components": {
        "schemas": {
            "RequestSchema": {
                "type": "array",
                "items": {
                    "type": "object",
                    "properties": {
                        "e1": {
                            "type": "string",
                            "enum": [
                                "a",
                                "b",
                                "c"
                            ]
                        }
                    },
                    "xml": {
                        "name": "item"
                    }
                },
                "xml": {
                    "wrapped": true,
                    "name": "arrayresponse"
                }
            }
        }
    }
}

More details

  • Python version: 3.7.5
  • Installed Packages:
    • Django==2.2.16
    • marshmallow==3.3.0
    • djangorestframework==3.10.3
    • djangorestframework-xml==2.0.0
    • drf-spectacular==0.15.1

I did some investigation in the library implementation and this part that causes the issue

# drf_spectacular/openapi.py

class AutoSchema(ViewInspector):
    def resolve_serializer(self, serializer, direction) -> ResolvedComponent:
        # ...
        keep_component = (
            or any(nest_tag in component.schema for nest_tag in ['oneOf', 'allOf', 'anyOf'])
            or component.schema.get('properties', {})
        )

        if not keep_component:
            del self.registry[component]
            return ResolvedComponent(None, None)  # sentinel
       # ...
@tfranzel
Copy link
Owner

hi @mohannad-musleh

this has been raised before in #351 and i have not gotten around investing time into a solution. we definitely have to address this. this problem has historical reasons as the array-ization is handled in other places. this can basically only happen with OpenApiSerializerExtension. i will give this another go

other things i noticed:

  • if you have control over MyViewSet, there is no need for an OpenApiViewExtension. @extend_schema_view(post/create=extend_schema(request=XXX)) or @extend_schema(request=XXX) def post() is technically equivalent.
  • MarshmallowSerializerExtension is missing a target class

@mohannad-musleh
Copy link
Author

@tfranzel thank you for your response.

sorry, somehow i missed issue #351.
I will try to find a temporary solution unit it fixed, any suggestions?

if you have control over MyViewSet, there is no need for an OpenApiViewExtension. @extend_schema_view(post/create=extend_schema(request=XXX)) or @extend_schema(request=XXX) def post() is technically equivalent.

I know, but I'm using OpenApiViewExtension to document the APIs for readability purposes, I have cases where the documentation logic (the docstring and the serializers classes initialization) larger than the actual implementation itself. so I decided to separate all the documentation logic from the actual implementation.

MarshmallowSerializerExtension is missing a target class

class MarshmallowSerializerExtension(OpenApiSerializerExtension):
    target_class = Schema # <-------- here
    match_subclasses = True

@tfranzel
Copy link
Owner

tfranzel commented May 19, 2021

temporary fix would be to change keep_component condition. i myself am still thinking about how to fix this cleanly. its a tricky issue to get right in all situations.

target_class = Schema # <-------- here

sry. i missed that line somehow.

@mohannad-musleh
Copy link
Author

temporary fix would be to change keep_component condition. i myself am still thinking about how to fix this cleanly. its a tricky issue to get right in all situations.

what is the issue with array as root element exactly? why excluded in the first place?

I can override the method implementation to fix the issue, but I like to keep the original implementation as much as possible. i think I will do that for now.

@tfranzel
Copy link
Owner

i'll try and come up with a solution. the rational is that serializers are often reused for list/retrieve. also "array or not" is often decided at the usage location. the code already unwraps this at those code locations (e.g. ListField/ Viewset.list). i did not account for that in extensions and thus this bug happens.

the question is what is the best solution?

  • unwrap the extension: possible but that breaks your usage of xml which i seen the first time there.
  • pass it though the keep_component

@tfranzel tfranzel added the bug Something isn't working label May 20, 2021
@mohannad-musleh
Copy link
Author

@tfranzel

suggestion: you can update the keep_component logic to include the schema if it's handled by a serializer extension class. this way the other areas will not be affected.

@tfranzel
Copy link
Owner

@mohannad-musleh yes that is what i was thinking. i need to go through the scenarios still but this is the most likely outcome.

@tfranzel
Copy link
Owner

please test the fix. i went a slightly different route as stated in #351

@tfranzel
Copy link
Owner

tfranzel commented Jun 1, 2021

closing this issue for now. feel free to comment if anything is missing or not working and we will follow-up.

@tfranzel tfranzel closed this as completed Jun 1, 2021
@mohannad-musleh
Copy link
Author

@tfranzel
I apologize for the late response.

I tested the fix, and it worked as expected.
Thank you!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working fix confirmation pending issue has been fixed and confirmation from issue reporter is pending
Projects
None yet
Development

No branches or pull requests

2 participants