Skip to content

Add max_length and min_length options to ListSerializer #8165

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

Merged
merged 1 commit into from
Sep 14, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions docs/api-guide/serializers.md
Original file line number Diff line number Diff line change
Expand Up @@ -755,6 +755,14 @@ The following argument can also be passed to a `ListSerializer` field or a seria

This is `True` by default, but can be set to `False` if you want to disallow empty lists as valid input.

### `max_length`

This is `None` by default, but can be set to a positive integer if you want to validates that the list contains no more than this number of elements.

### `min_length`

This is `None` by default, but can be set to a positive integer if you want to validates that the list contains no fewer than this number of elements.

### Customizing `ListSerializer` behavior

There *are* a few use cases when you might want to customize the `ListSerializer` behavior. For example:
Expand Down
27 changes: 25 additions & 2 deletions rest_framework/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,8 @@
LIST_SERIALIZER_KWARGS = (
'read_only', 'write_only', 'required', 'default', 'initial', 'source',
'label', 'help_text', 'style', 'error_messages', 'allow_empty',
'instance', 'data', 'partial', 'context', 'allow_null'
'instance', 'data', 'partial', 'context', 'allow_null',
'max_length', 'min_length'
)

ALL_FIELDS = '__all__'
Expand Down Expand Up @@ -143,12 +144,18 @@ def many_init(cls, *args, **kwargs):
return CustomListSerializer(*args, **kwargs)
"""
allow_empty = kwargs.pop('allow_empty', None)
max_length = kwargs.pop('max_length', None)
min_length = kwargs.pop('min_length', None)
child_serializer = cls(*args, **kwargs)
list_kwargs = {
'child': child_serializer,
}
if allow_empty is not None:
list_kwargs['allow_empty'] = allow_empty
if max_length is not None:
list_kwargs['max_length'] = max_length
if min_length is not None:
list_kwargs['min_length'] = min_length
list_kwargs.update({
key: value for key, value in kwargs.items()
if key in LIST_SERIALIZER_KWARGS
Expand Down Expand Up @@ -568,12 +575,16 @@ class ListSerializer(BaseSerializer):

default_error_messages = {
'not_a_list': _('Expected a list of items but got type "{input_type}".'),
'empty': _('This list may not be empty.')
'empty': _('This list may not be empty.'),
'max_length': _('Ensure this field has no more than {max_length} elements.'),
'min_length': _('Ensure this field has at least {min_length} elements.')
}

def __init__(self, *args, **kwargs):
self.child = kwargs.pop('child', copy.deepcopy(self.child))
self.allow_empty = kwargs.pop('allow_empty', True)
self.max_length = kwargs.pop('max_length', None)
self.min_length = kwargs.pop('min_length', None)
assert self.child is not None, '`child` is a required argument.'
assert not inspect.isclass(self.child), '`child` has not been instantiated.'
super().__init__(*args, **kwargs)
Expand Down Expand Up @@ -635,6 +646,18 @@ def to_internal_value(self, data):
api_settings.NON_FIELD_ERRORS_KEY: [message]
}, code='empty')

if self.max_length is not None and len(data) > self.max_length:
message = self.error_messages['max_length'].format(max_length=self.max_length)
raise ValidationError({
api_settings.NON_FIELD_ERRORS_KEY: [message]
}, code='max_length')

if self.min_length is not None and len(data) < self.min_length:
message = self.error_messages['min_length'].format(min_length=self.min_length)
raise ValidationError({
api_settings.NON_FIELD_ERRORS_KEY: [message]
}, code='min_length')

ret = []
errors = []

Expand Down
67 changes: 67 additions & 0 deletions tests/test_serializer_lists.py
Original file line number Diff line number Diff line change
Expand Up @@ -616,3 +616,70 @@ def test_nested_serializer_with_list_multipart(self):

assert serializer.is_valid()
assert serializer.validated_data == []


class TestMaxMinLengthListSerializer:
"""
Tests the behaviour of ListSerializers when max_length and min_length are used
"""

def setup(self):
class IntegerSerializer(serializers.Serializer):
some_int = serializers.IntegerField()

class MaxLengthSerializer(serializers.Serializer):
many_int = IntegerSerializer(many=True, max_length=5)

class MinLengthSerializer(serializers.Serializer):
many_int = IntegerSerializer(many=True, min_length=3)

class MaxMinLengthSerializer(serializers.Serializer):
many_int = IntegerSerializer(many=True, min_length=3, max_length=5)

self.MaxLengthSerializer = MaxLengthSerializer
self.MinLengthSerializer = MinLengthSerializer
self.MaxMinLengthSerializer = MaxMinLengthSerializer

def test_min_max_length_two_items(self):
input_data = {'many_int': [{'some_int': i} for i in range(2)]}

max_serializer = self.MaxLengthSerializer(data=input_data)
min_serializer = self.MinLengthSerializer(data=input_data)
max_min_serializer = self.MaxMinLengthSerializer(data=input_data)

assert max_serializer.is_valid()
assert max_serializer.validated_data == input_data

assert not min_serializer.is_valid()

assert not max_min_serializer.is_valid()

def test_min_max_length_four_items(self):
input_data = {'many_int': [{'some_int': i} for i in range(4)]}

max_serializer = self.MaxLengthSerializer(data=input_data)
min_serializer = self.MinLengthSerializer(data=input_data)
max_min_serializer = self.MaxMinLengthSerializer(data=input_data)

assert max_serializer.is_valid()
assert max_serializer.validated_data == input_data

assert min_serializer.is_valid()
assert min_serializer.validated_data == input_data

assert max_min_serializer.is_valid()
assert min_serializer.validated_data == input_data

def test_min_max_length_six_items(self):
input_data = {'many_int': [{'some_int': i} for i in range(6)]}

max_serializer = self.MaxLengthSerializer(data=input_data)
min_serializer = self.MinLengthSerializer(data=input_data)
max_min_serializer = self.MaxMinLengthSerializer(data=input_data)

assert not max_serializer.is_valid()

assert min_serializer.is_valid()
assert min_serializer.validated_data == input_data

assert not max_min_serializer.is_valid()