Skip to content

Commit

Permalink
add mechanism to handle custom ListSerializers with extensions
Browse files Browse the repository at this point in the history
  • Loading branch information
tfranzel committed May 22, 2022
1 parent efd8e7b commit 68e2b1b
Show file tree
Hide file tree
Showing 4 changed files with 82 additions and 8 deletions.
4 changes: 4 additions & 0 deletions docs/customization.rst
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,10 @@ This is one of the more involved extension mechanisms. *drf-spectacular* uses th
The usage of this extension is rarely necessary because most custom ``Serializer`` classes stay very
close to the default behaviour.

In case your ``Serializer`` makes use of a custom ``ListSerializer`` (i.e. a custom ``to_representation()``),
you can write a dedicated extensions for that. This is usually the case when ``many=True`` does not result
in a plain list, but rather in augmented object with additional fields (e.g. envelopes).

Declare custom/library filters with :py:class:`OpenApiFilterExtension <drf_spectacular.extensions.OpenApiFilterExtension>`
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Expand Down
22 changes: 16 additions & 6 deletions drf_spectacular/openapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,12 @@
assert_basic_serializer, build_array_type, build_basic_type, build_choice_field,
build_examples_list, build_generic_type, build_listed_example_value, build_media_type_object,
build_mocked_view, build_object_type, build_parameter_type, error, follow_field_source,
follow_model_field_lookup, force_instance, get_doc, get_type_hints, get_view_model,
is_basic_serializer, is_basic_type, is_field, is_list_serializer, is_patched_serializer,
is_serializer, is_trivial_string_variation, modify_media_types_for_versioning,
resolve_django_path_parameter, resolve_regex_path_parameter, resolve_type_hint, safe_ref,
sanitize_specification_extensions, warn, whitelisted,
follow_model_field_lookup, force_instance, get_doc, get_list_serializer, get_type_hints,
get_view_model, is_basic_serializer, is_basic_type, is_field, is_list_serializer,
is_list_serializer_customized, is_patched_serializer, is_serializer,
is_trivial_string_variation, modify_media_types_for_versioning, resolve_django_path_parameter,
resolve_regex_path_parameter, resolve_type_hint, safe_ref, sanitize_specification_extensions,
warn, whitelisted,
)
from drf_spectacular.settings import spectacular_settings
from drf_spectacular.types import OpenApiTypes
Expand Down Expand Up @@ -1330,7 +1331,16 @@ def _get_response_for_code(self, serializer, status_code, media_types=None):
and get_override(serializer, 'many') is not False
and ('200' <= status_code < '300' or spectacular_settings.ENABLE_LIST_MECHANICS_ON_NON_2XX)
):
schema = build_array_type(schema)
# In case of a non-default ListSerializer, check for matching extension and
# bypass regular list wrapping by delegating handling to extension.
if (
is_list_serializer_customized(serializer)
and OpenApiSerializerExtension.get_match(get_list_serializer(serializer))
):
schema = self._map_serializer(get_list_serializer(serializer), 'response')
else:
schema = build_array_type(schema)

paginator = self._get_paginator()

if (
Expand Down
12 changes: 12 additions & 0 deletions drf_spectacular/plumbing.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,18 @@ def is_list_serializer(obj) -> bool:
return isinstance(force_instance(obj), serializers.ListSerializer)


def get_list_serializer(obj):
return force_instance(obj) if is_list_serializer(obj) else get_class(obj)(many=True)


def is_list_serializer_customized(obj) -> bool:
return (
is_serializer(obj)
and get_class(get_list_serializer(obj)).to_representation # type: ignore
is not serializers.ListSerializer.to_representation
)


def is_basic_serializer(obj) -> bool:
return is_serializer(obj) and not is_list_serializer(obj)

Expand Down
52 changes: 50 additions & 2 deletions tests/test_extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@
from rest_framework.views import APIView

from drf_spectacular.extensions import (
OpenApiAuthenticationExtension, OpenApiSerializerFieldExtension, OpenApiViewExtension,
OpenApiAuthenticationExtension, OpenApiSerializerExtension, OpenApiSerializerFieldExtension,
OpenApiViewExtension,
)
from drf_spectacular.plumbing import (
ResolvedComponent, build_array_type, build_basic_type, build_object_type,
)
from drf_spectacular.plumbing import build_basic_type
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import extend_schema
from tests import generate_schema, get_response_schema
Expand Down Expand Up @@ -145,3 +148,48 @@ class XViewset(viewsets.ReadOnlyModelViewSet):
'appId': {'type': 'apiKey', 'in': 'header', 'name': 'X-APP-ID'}
}
assert schema['paths']['/x/']['get']['security'] == [{'apiKey': [], 'appId': []}]


def test_serializer_list_extension(no_warnings):

class CustomListSerializer(serializers.ListSerializer):
def to_representation(self, data):
return {'foo': 1, 'data': super().to_representation(data)} # pragma: no cover

# ListSerializer can be injected either via Meta attribute or by overriding many_init()
class XSerializer(serializers.ModelSerializer):
class Meta:
model = SimpleModel
fields = '__all__'
list_serializer_class = CustomListSerializer

class CustomListExtension(OpenApiSerializerExtension):
target_class = CustomListSerializer

def map_serializer(self, auto_schema, direction):
component = auto_schema.resolve_serializer(self.target.child, direction)
schema = build_object_type(
properties={'foo': build_basic_type(int), 'data': build_array_type(component.ref)}
)
list_component = ResolvedComponent(
name=f'{component.name}List',
type=ResolvedComponent.SCHEMA,
object=self.target.child,
schema=schema
)
auto_schema.registry.register_on_missing(list_component)
return list_component.ref

class XViewset(mixins.ListModelMixin, viewsets.GenericViewSet):
serializer_class = XSerializer

schema = generate_schema('x', XViewset)
op_schema = get_response_schema(schema['paths']['/x/']['get'])
assert op_schema == {'$ref': '#/components/schemas/XList'}
assert schema['components']['schemas']['XList'] == {
'type': 'object',
'properties': {
'foo': {'type': 'integer'},
'data': {'type': 'array', 'items': {'$ref': '#/components/schemas/X'}}
}
}

0 comments on commit 68e2b1b

Please sign in to comment.