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

Schema generation fails with AttributeError: 'NoneType' object has no attribute 'copy' #1342

Open
salomvary opened this issue Dec 4, 2024 · 3 comments

Comments

@salomvary
Copy link

Using Version: 0.28.0.

Describe the bug

As in title. Both when using the management command to generate the schema or the schema view.

The stack trace looks like this:

Traceback (most recent call last):
  File "./manage.py", line 23, in <module>
    main()
  File "./manage.py", line 19, in main
    execute_from_command_line(sys.argv)
  File ".venv/lib/python3.12/site-packages/django/core/management/__init__.py", line 442, in execute_from_command_line
    utility.execute()
  File ".venv/lib/python3.12/site-packages/django/core/management/__init__.py", line 436, in execute
    self.fetch_command(subcommand).run_from_argv(self.argv)
  File ".venv/lib/python3.12/site-packages/django/core/management/base.py", line 413, in run_from_argv
    self.execute(*args, **cmd_options)
  File ".venv/lib/python3.12/site-packages/django/core/management/base.py", line 459, in execute
    output = self.handle(*args, **options)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File ".venv/lib/python3.12/site-packages/drf_spectacular/management/commands/spectacular.py", line 72, in handle
    schema = generator.get_schema(request=None, public=True)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File ".venv/lib/python3.12/site-packages/drf_spectacular/generators.py", line 285, in get_schema
    paths=self.parse(request, public),
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File ".venv/lib/python3.12/site-packages/drf_spectacular/generators.py", line 256, in parse
    operation = view.schema.get_operation(
                ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File ".venv/lib/python3.12/site-packages/drf_spectacular/utils.py", line 451, in get_operation
    return super().get_operation(path, path_regex, path_prefix, method, registry)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File ".venv/lib/python3.12/site-packages/drf_spectacular/openapi.py", line 100, in get_operation
    request_body = self._get_request_body()
                   ^^^^^^^^^^^^^^^^^^^^^^^^
  File ".venv/lib/python3.12/site-packages/drf_spectacular/openapi.py", line 1321, in _get_request_body
    schema, partial_request_body_required = self._get_request_for_media_type(serializer, direction)
                                            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File ".venv/lib/python3.12/site-packages/drf_spectacular/openapi.py", line 1353, in _get_request_for_media_type
    component = self.resolve_serializer(serializer, direction)
                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File ".venv/lib/python3.12/site-packages/drf_spectacular/openapi.py", line 1648, in resolve_serializer
    component.schema = self._map_serializer(serializer, direction, bypass_extensions)
                       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File ".venv/lib/python3.12/site-packages/drf_spectacular/openapi.py", line 949, in _map_serializer
    schema = self._map_basic_serializer(serializer, direction)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File ".venv/lib/python3.12/site-packages/drf_spectacular/openapi.py", line 1048, in _map_basic_serializer
    schema = self._map_serializer_field(field, direction)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File ".venv/lib/python3.12/site-packages/drf_spectacular/openapi.py", line 658, in _map_serializer_field
    return append_meta(schema, meta)
           ^^^^^^^^^^^^^^^^^^^^^^^^^
  File ".venv/lib/python3.12/site-packages/drf_spectacular/plumbing.py", line 542, in append_meta
    schema = schema.copy()
             ^^^^^^^^^^^
AttributeError: 'NoneType' object has no attribute 'copy'

To Reproduce
I've tried but failed to create a snippet, the problem only occurs a specific, rather complex project.

The change that triggered the issue looks something like this:

    class X(models.Model):
        position = models.IntegerField()

    class ParentSerializer(serializers.Serializer):
        field = serializers.SerializerMethodField()

        @extend_schema_field(int)
        def get_field(self, instance):
            return None

    @extend_schema_field(ParentSerializer)
    class XChildSerializer(ParentSerializer):
        pass

    class XSerializer(serializers.ModelSerializer):
        field = XChildSerializer(read_only=True, source="*")

        class Meta:
            model = X
            fields = ("id", "position", "field")

    class XView(views.APIView):
        serializer_class = XSerializer

        def get(self, request):
            pass  # pragma: no cover

        def post(self, request):
            pass  # pragma: no cover

The interesting part is annotating XChildSerializer with @extend_schema_field(ParentSerializer). If I remove the annotation, the problem goes away. In my real project I also have another YChildSerializer with the same annotation, and that does not have this problem.

Expected behavior

Schema should be generated without exception.

I've been trying to figure out what might be wrong using the debugger, but I don't understand the internals of drf-spectacular enough to have any clue. Any pointers would be appreciated.

@salomvary
Copy link
Author

This is how far I got trying to reproduce the problem by adding a test case to test_regressions.py, but this test passes:

@mock.patch('drf_spectacular.settings.spectacular_settings.OAS_VERSION', '3.1.0')
def test_extend_schema_field_on_serializer(no_warnings):
    class X(models.Model):
        position = models.IntegerField()

    class ParentSerializer(serializers.Serializer):
        field = serializers.SerializerMethodField()

        @extend_schema_field(int)
        def get_field(self, instance):
            return None

    @extend_schema_field(ParentSerializer)
    class XChildSerializer(ParentSerializer):
        pass

    class XSerializer(serializers.ModelSerializer):
        field = XChildSerializer(read_only=True, source="*")

        class Meta:
            model = X
            fields = ("id", "position", "field")

    class XView(views.APIView):
        serializer_class = XSerializer

        def get(self, request):
            pass  # pragma: no cover

        def post(self, request):
            pass  # pragma: no cover

    @extend_schema_field(ParentSerializer)
    class YChildSerializer(ParentSerializer):
        pass

    class YSerializer(serializers.ModelSerializer):
        field = YChildSerializer()

        class Meta:
            model = X
            fields = ("id", "position", "field")

    class YView(views.APIView):
        serializer_class = YSerializer

        def get(self, request):
            pass  # pragma: no cover

        def post(self, request):
            pass  # pragma: no cover

    schema = generate_schema(None, patterns=[path('x', XView.as_view()), path('y', YView.as_view())])
    assert 'X' in schema['components']['schemas']
    assert 'Y' in schema['components']['schemas']
    assert 'Parent' in schema['components']['schemas']
    assert 'XChild' not in schema['components']['schemas']

@tfranzel
Copy link
Owner

tfranzel commented Dec 5, 2024

Hi

The interesting part is annotating XChildSerializer with @extend_schema_field(ParentSerializer). If I remove the annotation, the problem goes away. In my real project I also have another YChildSerializer with the same annotation, and that does not have this problem.

so according to the trace, the schema ends up as None, which is never supposed to happen. The issue happened a bit earlier in

schema = self._map_serializer_field(force_instance(override), direction) # line 645

while processing the override. The decoration itself is fine and should work. I suspect there is a problem with the ParentSerializer, and your example is to simple to trigger the error.

I can't really do much here without a reproduction. This code ran millions of times without issue, so you must have hit some rare edge-case. Please try to create a more accurate version of the offending serializer. I would even so far and say serializer would throw even used regularly in a view (without the decoration), as it hits the code in pretty much exactly the same way.

@salomvary
Copy link
Author

@tfranzel Thanks for the insights, I am also suspecting an edge case, will try harder to reproduce/debug.

For now my ugly workaround is to change the failing annotation to:

@extend_schema_field(
    {"allOf": {"$ref": "#/components/schemas/Parent"}, "readOnly": True}
)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants