diff --git a/docs/api-guide/serializers.md b/docs/api-guide/serializers.md index 13c0c8710..cf8525748 100644 --- a/docs/api-guide/serializers.md +++ b/docs/api-guide/serializers.md @@ -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: diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 9ea57f1af..389680517 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -71,7 +71,8 @@ from rest_framework.relations import Hyperlink, PKOnlyObject # NOQA # isort:ski 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__' @@ -143,12 +144,18 @@ class BaseSerializer(Field): 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 @@ -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) @@ -635,6 +646,18 @@ class ListSerializer(BaseSerializer): 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 = [] diff --git a/tests/test_serializer_lists.py b/tests/test_serializer_lists.py index f35c4fcc9..551f62666 100644 --- a/tests/test_serializer_lists.py +++ b/tests/test_serializer_lists.py @@ -616,3 +616,70 @@ class TestEmptyListSerializer: 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()