diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index ca63192888..42910e13f5 100644 --- a/docs/api-guide/fields.md +++ b/docs/api-guide/fields.md @@ -471,12 +471,13 @@ Requires either the `Pillow` package or `PIL` package. The `Pillow` package is A field class that validates a list of objects. -**Signature**: `ListField(child=, allow_empty=True, min_length=None, max_length=None)` +**Signature**: `ListField(child=, allow_empty=True, min_length=None, max_length=None, max_depth=None)` * `child` - A field instance that should be used for validating the objects in the list. If this argument is not provided then objects in the list will not be validated. * `allow_empty` - Designates if empty lists are allowed. * `min_length` - Validates that the list contains no fewer than this number of elements. * `max_length` - Validates that the list contains no more than this number of elements. +* `max_depth` - Validates that nesting depth does not exceed this value. This applies to both the field schema depth and the raw input data depth. A value of 1 permits a flat structure (e.g., `[1, 2, 3]`) but rejects nested data (e.g., `[[1, 2]]`). Defaults to `None` (no limit). For example, to validate a list of integers you might use something like the following: @@ -495,10 +496,11 @@ We can now reuse our custom `StringListField` class throughout our application, A field class that validates a dictionary of objects. The keys in `DictField` are always assumed to be string values. -**Signature**: `DictField(child=, allow_empty=True)` +**Signature**: `DictField(child=, allow_empty=True, max_depth=None)` * `child` - A field instance that should be used for validating the values in the dictionary. If this argument is not provided then values in the mapping will not be validated. * `allow_empty` - Designates if empty dictionaries are allowed. +* `max_depth` - Validates that nesting depth does not exceed this value. This applies to both the field schema depth and the raw input data depth. A value of 1 permits a flat structure (e.g., `{"a": 1}`) but rejects nested data (e.g., `{"a": {"b": 1}}`). Defaults to `None` (no limit). For example, to create a field that validates a mapping of strings to strings, you would write something like this: diff --git a/rest_framework/fields.py b/rest_framework/fields.py index f5009a7303..ae1be289bb 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -1650,7 +1650,8 @@ class ListField(Field): 'not_a_list': _('Expected a list of items but got type "{input_type}".'), 'empty': _('This list may not be empty.'), 'min_length': _('Ensure this field has at least {min_length} elements.'), - 'max_length': _('Ensure this field has no more than {max_length} elements.') + 'max_length': _('Ensure this field has no more than {max_length} elements.'), + 'max_depth': _('List nesting depth exceeds maximum allowed depth of {max_depth}.') } def __init__(self, **kwargs): @@ -1658,6 +1659,7 @@ def __init__(self, **kwargs): self.allow_empty = kwargs.pop('allow_empty', True) self.max_length = kwargs.pop('max_length', None) self.min_length = kwargs.pop('min_length', None) + self.max_depth = kwargs.pop('max_depth', None) assert not inspect.isclass(self.child), '`child` has not been instantiated.' assert self.child.source is None, ( @@ -1666,6 +1668,8 @@ def __init__(self, **kwargs): ) super().__init__(**kwargs) + self._current_depth = 0 + self._root_max_depth = self.max_depth self.child.bind(field_name='', parent=self) if self.max_length is not None: message = lazy_format(self.error_messages['max_length'], max_length=self.max_length) @@ -1674,6 +1678,29 @@ def __init__(self, **kwargs): message = lazy_format(self.error_messages['min_length'], min_length=self.min_length) self.validators.append(MinLengthValidator(self.min_length, message=message)) + def bind(self, field_name, parent): + super().bind(field_name, parent) + if self.max_depth is None and hasattr(parent, '_root_max_depth') and parent._root_max_depth is not None: + self._root_max_depth = parent._root_max_depth + self._current_depth = parent._current_depth + 1 + self._propagate_depth_to_child() + + def _propagate_depth_to_child(self): + if self._root_max_depth is not None and hasattr(self.child, '_root_max_depth'): + self.child._root_max_depth = self._root_max_depth + self.child._current_depth = self._current_depth + 1 + if hasattr(self.child, '_propagate_depth_to_child'): + self.child._propagate_depth_to_child() + + def _check_data_depth(self, data, current_level): + items = data.values() if isinstance(data, dict) else data + for item in items: + if isinstance(item, (list, tuple, dict)): + next_level = current_level + 1 + if next_level > self._root_max_depth: + self.fail('max_depth', max_depth=self._root_max_depth) + self._check_data_depth(item, next_level) + def get_value(self, dictionary): if self.field_name not in dictionary: if getattr(self.root, 'partial', False): @@ -1699,6 +1726,12 @@ def to_internal_value(self, data): self.fail('not_a_list', input_type=type(data).__name__) if not self.allow_empty and len(data) == 0: self.fail('empty') + if self._root_max_depth is not None: + start_level = self._current_depth if self._current_depth > 0 else 1 + if start_level > self._root_max_depth: + self.fail('max_depth', max_depth=self._root_max_depth) + if self.max_depth is not None: + self._check_data_depth(data, start_level) return self.run_child_validation(data) def to_representation(self, data): @@ -1730,11 +1763,13 @@ class DictField(Field): default_error_messages = { 'not_a_dict': _('Expected a dictionary of items but got type "{input_type}".'), 'empty': _('This dictionary may not be empty.'), + 'max_depth': _('Dictionary nesting depth exceeds maximum allowed depth of {max_depth}.') } def __init__(self, **kwargs): self.child = kwargs.pop('child', copy.deepcopy(self.child)) self.allow_empty = kwargs.pop('allow_empty', True) + self.max_depth = kwargs.pop('max_depth', None) assert not inspect.isclass(self.child), '`child` has not been instantiated.' assert self.child.source is None, ( @@ -1743,8 +1778,33 @@ def __init__(self, **kwargs): ) super().__init__(**kwargs) + self._current_depth = 0 + self._root_max_depth = self.max_depth self.child.bind(field_name='', parent=self) + def bind(self, field_name, parent): + super().bind(field_name, parent) + if self.max_depth is None and hasattr(parent, '_root_max_depth') and parent._root_max_depth is not None: + self._root_max_depth = parent._root_max_depth + self._current_depth = parent._current_depth + 1 + self._propagate_depth_to_child() + + def _propagate_depth_to_child(self): + if self._root_max_depth is not None and hasattr(self.child, '_root_max_depth'): + self.child._root_max_depth = self._root_max_depth + self.child._current_depth = self._current_depth + 1 + if hasattr(self.child, '_propagate_depth_to_child'): + self.child._propagate_depth_to_child() + + def _check_data_depth(self, data, current_level): + items = data.values() if isinstance(data, dict) else data + for item in items: + if isinstance(item, (list, tuple, dict)): + next_level = current_level + 1 + if next_level > self._root_max_depth: + self.fail('max_depth', max_depth=self._root_max_depth) + self._check_data_depth(item, next_level) + def get_value(self, dictionary): # We override the default field access in order to support # dictionaries in HTML forms. @@ -1762,7 +1822,12 @@ def to_internal_value(self, data): self.fail('not_a_dict', input_type=type(data).__name__) if not self.allow_empty and len(data) == 0: self.fail('empty') - + if self._root_max_depth is not None: + start_level = self._current_depth if self._current_depth > 0 else 1 + if start_level > self._root_max_depth: + self.fail('max_depth', max_depth=self._root_max_depth) + if self.max_depth is not None: + self._check_data_depth(data, start_level) return self.run_child_validation(data) def to_representation(self, value): diff --git a/rest_framework/locale/en/LC_MESSAGES/django.po b/rest_framework/locale/en/LC_MESSAGES/django.po index 99c57b40c5..128be6c674 100644 --- a/rest_framework/locale/en/LC_MESSAGES/django.po +++ b/rest_framework/locale/en/LC_MESSAGES/django.po @@ -1,7 +1,7 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. -# +# # Translators: msgid "" msgstr "" @@ -357,6 +357,11 @@ msgstr "Ensure this field has at least {min_length} elements." msgid "Ensure this field has no more than {max_length} elements." msgstr "Ensure this field has no more than {max_length} elements." +#: fields.py:1654 +#, python-brace-format +msgid "List nesting depth exceeds maximum allowed depth of {max_depth}." +msgstr "List nesting depth exceeds maximum allowed depth of {max_depth}." + #: fields.py:1682 #, python-brace-format msgid "Expected a dictionary of items but got type \"{input_type}\"." @@ -366,6 +371,11 @@ msgstr "Expected a dictionary of items but got type \"{input_type}\"." msgid "This dictionary may not be empty." msgstr "This dictionary may not be empty." +#: fields.py:1733 +#, python-brace-format +msgid "Dictionary nesting depth exceeds maximum allowed depth of {max_depth}." +msgstr "Dictionary nesting depth exceeds maximum allowed depth of {max_depth}." + #: fields.py:1755 msgid "Value must be valid JSON." msgstr "Value must be valid JSON." diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index ca60810df1..f2d24174cf 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -76,9 +76,9 @@ 'read_only', 'write_only', 'required', 'default', 'initial', 'source', 'label', 'help_text', 'style', 'error_messages', 'allow_empty', 'instance', 'data', 'partial', 'context', 'allow_null', - 'max_length', 'min_length' + 'max_length', 'min_length', 'max_depth' ) -LIST_SERIALIZER_KWARGS_REMOVE = ('allow_empty', 'min_length', 'max_length') +LIST_SERIALIZER_KWARGS_REMOVE = ('allow_empty', 'min_length', 'max_length', 'max_depth') ALL_FIELDS = '__all__' @@ -111,6 +111,10 @@ class BaseSerializer(Field): .data - Available. """ + default_error_messages = { + 'max_depth': _('Nesting depth exceeds maximum allowed depth of {max_depth}.') + } + def __init__(self, instance=None, data=empty, **kwargs): self.instance = instance if data is not empty: @@ -118,7 +122,51 @@ def __init__(self, instance=None, data=empty, **kwargs): self.partial = kwargs.pop('partial', False) self._context = kwargs.pop('context', {}) kwargs.pop('many', None) + self.max_depth = kwargs.pop('max_depth', None) super().__init__(**kwargs) + self._current_depth = 0 + self._root_max_depth = self.max_depth + + def bind(self, field_name, parent): + super().bind(field_name, parent) + if self.max_depth is None and hasattr(parent, '_root_max_depth') and parent._root_max_depth is not None: + self._root_max_depth = parent._root_max_depth + self._current_depth = parent._current_depth + 1 + + def _propagate_depth_to_child(self): + if self._root_max_depth is not None and 'fields' in self.__dict__: + for field in self.__dict__['fields'].values(): + if hasattr(field, '_root_max_depth'): + field._root_max_depth = self._root_max_depth + field._current_depth = self._current_depth + 1 + if hasattr(field, '_propagate_depth_to_child'): + field._propagate_depth_to_child() + + def _check_data_depth(self, data, current_level): + if isinstance(data, dict): + for value in data.values(): + if isinstance(value, (list, tuple, dict)): + next_level = current_level + 1 + if next_level > self._root_max_depth: + message = self.error_messages['max_depth'].format( + max_depth=self._root_max_depth + ) + raise ValidationError({ + api_settings.NON_FIELD_ERRORS_KEY: [message] + }, code='max_depth') + self._check_data_depth(value, next_level) + elif isinstance(data, (list, tuple)): + for item in data: + if isinstance(item, (list, tuple, dict)): + next_level = current_level + 1 + if next_level > self._root_max_depth: + message = self.error_messages['max_depth'].format( + max_depth=self._root_max_depth + ) + raise ValidationError({ + api_settings.NON_FIELD_ERRORS_KEY: [message] + }, code='max_depth') + self._check_data_depth(item, next_level) def __new__(cls, *args, **kwargs): # We override this method in order to automatically create @@ -373,6 +421,8 @@ def fields(self): fields = BindingDict(self) for key, value in self.get_fields().items(): fields[key] = value + if self._root_max_depth is not None: + self._propagate_depth_to_child() return fields @property @@ -489,6 +539,9 @@ def to_internal_value(self, data): raise ValidationError({ api_settings.NON_FIELD_ERRORS_KEY: [message] }, code='invalid') + if self._root_max_depth is not None and self.max_depth is not None: + start_level = self._current_depth + self._check_data_depth(data, start_level) ret = {} errors = {} @@ -654,6 +707,32 @@ def run_child_validation(self, data): """ return self.child.run_validation(data) + def _check_data_depth(self, data, current_level): + if isinstance(data, (list, tuple)): + for item in data: + if isinstance(item, (list, tuple, dict)): + next_level = current_level + 1 + if next_level > self._root_max_depth: + message = self.error_messages['max_depth'].format( + max_depth=self._root_max_depth + ) + raise ValidationError({ + api_settings.NON_FIELD_ERRORS_KEY: [message] + }, code='max_depth') + self._check_data_depth(item, next_level) + elif isinstance(data, dict): + for value in data.values(): + if isinstance(value, (list, tuple, dict)): + next_level = current_level + 1 + if next_level > self._root_max_depth: + message = self.error_messages['max_depth'].format( + max_depth=self._root_max_depth + ) + raise ValidationError({ + api_settings.NON_FIELD_ERRORS_KEY: [message] + }, code='max_depth') + self._check_data_depth(value, next_level) + def to_internal_value(self, data): """ List of dicts of native values <- List of dicts of primitive datatypes. @@ -669,6 +748,10 @@ def to_internal_value(self, data): api_settings.NON_FIELD_ERRORS_KEY: [message] }, code='not_a_list') + if self._root_max_depth is not None and self.max_depth is not None: + start_level = self._current_depth + self._check_data_depth(data, start_level) + if not self.allow_empty and len(data) == 0: message = self.error_messages['empty'] raise ValidationError({ diff --git a/tests/test_fields.py b/tests/test_fields.py index e360793184..62a9db994e 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -2545,6 +2545,474 @@ class TestUnvalidatedDictField(FieldValues): field = serializers.DictField() +class TestListFieldMaxDepth: + def test_flat_list_with_max_depth_none(self): + field = serializers.ListField(child=serializers.IntegerField(), max_depth=None) + output = field.run_validation([1, 2, 3]) + assert output == [1, 2, 3] + + def test_nested_list_with_max_depth_none(self): + field = serializers.ListField( + child=serializers.ListField(child=serializers.ListField(child=serializers.IntegerField())), + max_depth=None + ) + output = field.run_validation([[[1, 2]], [[3]]]) + assert output == [[[1, 2]], [[3]]] + + def test_max_depth_zero_rejects_everything(self): + field = serializers.ListField(child=serializers.IntegerField(), max_depth=0) + with pytest.raises(serializers.ValidationError) as exc_info: + field.run_validation([1, 2, 3]) + assert 'max_depth' in str(exc_info.value.detail) + + def test_max_depth_one_allows_flat_list(self): + field = serializers.ListField(child=serializers.IntegerField(), max_depth=1) + output = field.run_validation([1, 2, 3]) + assert output == [1, 2, 3] + + def test_max_depth_one_rejects_nested_list(self): + field = serializers.ListField(child=serializers.IntegerField(), max_depth=1) + with pytest.raises(serializers.ValidationError) as exc_info: + field.run_validation([[1, 2]]) + assert 'max_depth' in str(exc_info.value.detail) + + def test_max_depth_two_allows_one_level_nesting(self): + field = serializers.ListField( + child=serializers.ListField(child=serializers.IntegerField()), + max_depth=2 + ) + output = field.run_validation([[1, 2], [3, 4]]) + assert output == [[1, 2], [3, 4]] + + def test_max_depth_two_rejects_two_levels_nesting(self): + field = serializers.ListField( + child=serializers.ListField(child=serializers.IntegerField()), + max_depth=2 + ) + with pytest.raises(serializers.ValidationError) as exc_info: + field.run_validation([[[1, 2]], [[3]]]) + assert 'max_depth' in str(exc_info.value.detail) + + def test_deeply_nested_exceeds_max_depth(self): + field = serializers.ListField( + child=serializers.ListField( + child=serializers.ListField( + child=serializers.ListField( + child=serializers.ListField(child=serializers.IntegerField()) + ) + ) + ), + max_depth=3 + ) + with pytest.raises(serializers.ValidationError): + field.run_validation([[[[[1]]]]]) + + def test_max_depth_with_mixed_nesting_list_and_dict(self): + field = serializers.ListField( + child=serializers.DictField(child=serializers.ListField(child=serializers.IntegerField())), + max_depth=3 + ) + output = field.run_validation([{'a': [1, 2], 'b': [3]}]) + assert output == [{'a': [1, 2], 'b': [3]}] + + def test_max_depth_with_mixed_nesting_exceeds_limit(self): + field = serializers.ListField( + child=serializers.DictField( + child=serializers.ListField(child=serializers.ListField(child=serializers.IntegerField())) + ), + max_depth=2 + ) + with pytest.raises(serializers.ValidationError): + field.run_validation([{'a': [[1, 2]]}]) + + def test_error_message_contains_max_depth_value(self): + field = serializers.ListField( + child=serializers.ListField(child=serializers.IntegerField()), + max_depth=0 + ) + with pytest.raises(serializers.ValidationError) as exc_info: + field.run_validation([[1, 2]]) + error_msg = str(exc_info.value.detail[0]) + assert '0' in error_msg + + +class TestDictFieldMaxDepth: + def test_flat_dict_with_max_depth_none(self): + field = serializers.DictField(child=serializers.IntegerField(), max_depth=None) + output = field.run_validation({'a': 1, 'b': 2}) + assert output == {'a': 1, 'b': 2} + + def test_nested_dict_with_max_depth_none(self): + field = serializers.DictField( + child=serializers.DictField(child=serializers.DictField(child=serializers.IntegerField())), + max_depth=None + ) + output = field.run_validation({'a': {'b': {'c': 1}}}) + assert output == {'a': {'b': {'c': 1}}} + + def test_max_depth_zero_rejects_everything(self): + field = serializers.DictField(child=serializers.IntegerField(), max_depth=0) + with pytest.raises(serializers.ValidationError) as exc_info: + field.run_validation({'a': 1, 'b': 2}) + assert 'max_depth' in str(exc_info.value.detail) + + def test_max_depth_one_allows_flat_dict(self): + field = serializers.DictField(child=serializers.IntegerField(), max_depth=1) + output = field.run_validation({'a': 1, 'b': 2}) + assert output == {'a': 1, 'b': 2} + + def test_max_depth_one_rejects_two_levels_nesting(self): + field = serializers.DictField( + child=serializers.DictField(child=serializers.DictField(child=serializers.IntegerField())), + max_depth=1 + ) + with pytest.raises(serializers.ValidationError) as exc_info: + field.run_validation({'a': {'b': {'c': 1}}}) + assert 'max_depth' in str(exc_info.value.detail) + + def test_max_depth_with_mixed_nesting_dict_and_list(self): + field = serializers.DictField( + child=serializers.ListField(child=serializers.DictField(child=serializers.IntegerField())), + max_depth=3 + ) + output = field.run_validation({'a': [{'b': 1, 'c': 2}]}) + assert output == {'a': [{'b': 1, 'c': 2}]} + + def test_max_depth_with_mixed_nesting_exceeds_limit(self): + field = serializers.DictField( + child=serializers.ListField( + child=serializers.DictField(child=serializers.DictField(child=serializers.IntegerField())) + ), + max_depth=2 + ) + with pytest.raises(serializers.ValidationError): + field.run_validation({'a': [{'b': {'c': 1}}]}) + + def test_error_message_contains_max_depth_value(self): + field = serializers.DictField( + child=serializers.DictField(child=serializers.IntegerField()), + max_depth=0 + ) + with pytest.raises(serializers.ValidationError) as exc_info: + field.run_validation({'a': {'b': 1}}) + error_msg = str(exc_info.value.detail) + assert '0' in error_msg + + +class TestMaxDepthEdgeCases: + def test_field_reuse_does_not_leak_depth_state(self): + child_field = serializers.ListField(child=serializers.IntegerField()) + field = serializers.ListField(child=child_field, max_depth=2) + output1 = field.run_validation([[1, 2], [3, 4]]) + assert output1 == [[1, 2], [3, 4]] + output2 = field.run_validation([[5, 6], [7, 8]]) + assert output2 == [[5, 6], [7, 8]] + + def test_max_depth_with_empty_nested_structures(self): + field = serializers.ListField( + child=serializers.ListField(child=serializers.IntegerField()), + max_depth=2 + ) + output = field.run_validation([[], []]) + assert output == [[], []] + + def test_very_deep_nesting_rejected_immediately(self): + child = serializers.IntegerField() + for _ in range(10): + child = serializers.ListField(child=child) + field = serializers.ListField(child=child, max_depth=5) + data = [1] + for _ in range(10): + data = [data] + with pytest.raises(serializers.ValidationError): + field.run_validation(data) + + +class TestMaxDepthDataInspection: + def test_flat_field_rejects_deeply_nested_data(self): + field = serializers.ListField(max_depth=1) + field.run_validation([1, 2, 3]) + with pytest.raises(serializers.ValidationError) as exc_info: + field.run_validation([[1, 2]]) + assert 'max_depth' in str(exc_info.value.detail) + + def test_flat_dict_field_rejects_deeply_nested_data(self): + field = serializers.DictField(max_depth=1) + field.run_validation({'a': 1, 'b': 2}) + with pytest.raises(serializers.ValidationError) as exc_info: + field.run_validation({'a': {'b': 1}}) + assert 'max_depth' in str(exc_info.value.detail) + + def test_max_depth_zero_rejects_any_nesting_in_data(self): + field = serializers.ListField(max_depth=0) + with pytest.raises(serializers.ValidationError): + field.run_validation([1, 2, 3]) + + def test_data_depth_check_with_mixed_structures(self): + field = serializers.ListField(max_depth=1) + field.run_validation([1, 2, 3]) + with pytest.raises(serializers.ValidationError): + field.run_validation([{'a': 1}, [2], 3]) + + def test_dict_field_data_depth_with_nested_lists(self): + field = serializers.DictField(max_depth=1) + field.run_validation({'a': 1, 'b': 2}) + with pytest.raises(serializers.ValidationError): + field.run_validation({'a': [1, 2]}) + + def test_data_depth_respects_current_depth(self): + inner = serializers.ListField(child=serializers.IntegerField()) + outer = serializers.ListField(child=inner, max_depth=2) + outer.run_validation([[1, 2], [3, 4]]) + with pytest.raises(serializers.ValidationError): + outer.run_validation([[[1]]]) + + def test_max_depth_one_means_flat_only(self): + field = serializers.ListField(child=serializers.IntegerField(), max_depth=1) + field.run_validation([1, 2, 3]) + with pytest.raises(serializers.ValidationError) as exc_info: + field.run_validation([[1, 2]]) + assert 'max_depth' in str(exc_info.value.detail) + + def test_serializer_with_list_field_respects_depth(self): + class TestSerializer(serializers.Serializer): + data = serializers.ListField(child=serializers.IntegerField(), max_depth=1) + + serializer = TestSerializer(data={'data': [1, 2, 3]}) + assert serializer.is_valid() + + serializer = TestSerializer(data={'data': [[1, 2]]}) + assert not serializer.is_valid() + assert 'max_depth' in str(serializer.errors) + + def test_serializer_with_max_depth_inspects_raw_data(self): + class TestSerializer(serializers.Serializer): + name = serializers.CharField() + value = serializers.IntegerField() + + serializer = TestSerializer(data={'name': 'test', 'value': 1}, max_depth=1) + assert serializer.is_valid() + + serializer = TestSerializer(data={'name': 'test', 'value': {'nested': 1}}, max_depth=1) + assert not serializer.is_valid() + assert 'max_depth' in str(serializer.errors) or 'invalid' in str(serializer.errors) + + def test_standalone_serializer_protects_against_deep_json(self): + class SimpleSerializer(serializers.Serializer): + data = serializers.CharField() + + serializer = SimpleSerializer(data={'data': 'value'}, max_depth=1) + assert serializer.is_valid() + + deep_data = {'data': {'nested': {'deep': 'value'}}} + serializer = SimpleSerializer(data=deep_data, max_depth=1) + assert not serializer.is_valid() + assert 'max_depth' in str(serializer.errors) + + def test_list_serializer_many_true_respects_max_depth(self): + class MySerializer(serializers.Serializer): + name = serializers.CharField() + + serializer = MySerializer(data=[{'name': 'test'}], many=True, max_depth=1) + assert serializer.is_valid() + + deep_list_data = [{'name': 'test'}, [1, 2, 3]] + serializer = MySerializer(data=deep_list_data, many=True, max_depth=1) + assert not serializer.is_valid() + + def test_list_serializer_protects_against_deeply_nested_lists(self): + class ItemSerializer(serializers.Serializer): + value = serializers.IntegerField() + + serializer = ItemSerializer(data=[{'value': 1}, {'value': 2}], many=True, max_depth=1) + assert serializer.is_valid() + + deep_data = [{'value': {'nested': 1}}] + serializer = ItemSerializer(data=deep_data, many=True, max_depth=1) + assert not serializer.is_valid() + + +class TestMaxDepthWithSerializers: + def test_list_field_containing_serializer_with_nested_list(self): + class InnerSerializer(serializers.Serializer): + numbers = serializers.ListField(child=serializers.IntegerField()) + + field = serializers.ListField(child=InnerSerializer(), max_depth=3) + valid_data = [{'numbers': [1, 2]}, {'numbers': [3, 4]}] + output = field.run_validation(valid_data) + assert output == [{'numbers': [1, 2]}, {'numbers': [3, 4]}] + + def test_list_field_containing_serializer_exceeds_max_depth(self): + class InnerSerializer(serializers.Serializer): + nested_list = serializers.ListField( + child=serializers.ListField(child=serializers.IntegerField()) + ) + + field = serializers.ListField(child=InnerSerializer(), max_depth=3) + with pytest.raises(serializers.ValidationError): + field.run_validation([{'nested_list': [[1, 2]]}]) + + def test_serializer_within_dict_field_respects_depth(self): + class ValueSerializer(serializers.Serializer): + data = serializers.ListField(child=serializers.IntegerField()) + + field = serializers.DictField(child=ValueSerializer(), max_depth=3) + valid_data = {'key1': {'data': [1, 2]}, 'key2': {'data': [3, 4]}} + output = field.run_validation(valid_data) + assert output == {'key1': {'data': [1, 2]}, 'key2': {'data': [3, 4]}} + + def test_deeply_nested_serializer_structure_rejected(self): + class Level3Serializer(serializers.Serializer): + values = serializers.ListField(child=serializers.IntegerField()) + + class Level2Serializer(serializers.Serializer): + level3 = Level3Serializer() + + class Level1Serializer(serializers.Serializer): + level2 = Level2Serializer() + + field = serializers.ListField(child=Level1Serializer(), max_depth=4) + with pytest.raises(serializers.ValidationError): + field.run_validation([{'level2': {'level3': {'values': [1, 2]}}}]) + + +class TestListFieldBindWithExplicitMaxDepth: + def test_child_listfield_keeps_explicit_max_depth_zero(self): + inner_list = serializers.ListField( + child=serializers.IntegerField(), + max_depth=0 + ) + outer_list = serializers.ListField( + child=inner_list, + max_depth=10 + ) + assert inner_list._root_max_depth == 0 + with pytest.raises(serializers.ValidationError): + outer_list.run_validation([[1, 2]]) + + +class TestListFieldBindWithoutExplicitMaxDepth: + def test_child_without_max_depth_inherits_from_parent(self): + inner_list = serializers.ListField( + child=serializers.IntegerField() + ) + outer_list = serializers.ListField( + child=inner_list, + max_depth=3 + ) + assert inner_list._root_max_depth == 3 + assert inner_list._current_depth == 1 + output = outer_list.run_validation([[1, 2]]) + assert output == [[1, 2]] + with pytest.raises(serializers.ValidationError): + outer_list.run_validation([[[[1]]]]) + + def test_multiple_children_some_inherit_some_explicit(self): + child_a = serializers.ListField( + child=serializers.IntegerField(), + max_depth=2 + ) + child_b = serializers.ListField( + child=serializers.IntegerField() + ) + child_c = serializers.ListField( + child=serializers.IntegerField(), + max_depth=4 + ) + + class MySerializerA(serializers.Serializer): + field = child_a + + class MySerializerB(serializers.Serializer): + field = child_b + + class MySerializerC(serializers.Serializer): + field = child_c + + s_a = MySerializerA(max_depth=5) + s_b = MySerializerB(max_depth=5) + s_c = MySerializerC(max_depth=5) + + assert s_a.fields['field']._root_max_depth == 2 + assert s_b.fields['field']._root_max_depth == 5 + assert s_c.fields['field']._root_max_depth == 4 + + +class TestListFieldBindDepthPropagation: + def test_depth_propagation_through_multiple_levels(self): + level3 = serializers.ListField(child=serializers.IntegerField()) + level2 = serializers.ListField(child=level3) + level1 = serializers.ListField(child=level2, max_depth=10) + + assert level1._current_depth == 0 + assert level2._current_depth == 1 + assert level3._current_depth == 2 + assert level1._root_max_depth == 10 + assert level2._root_max_depth == 10 + assert level3._root_max_depth == 10 + + +class TestListFieldBindWithDictField: + def test_listfield_child_of_dictfield_keeps_explicit_max_depth(self): + list_field = serializers.ListField( + child=serializers.IntegerField(), + max_depth=2 + ) + dict_field = serializers.DictField( + child=list_field, + max_depth=10 + ) + assert dict_field.child._root_max_depth == 2 + output = dict_field.run_validation({'key': [1, 2]}) + assert output == {'key': [1, 2]} + with pytest.raises(serializers.ValidationError): + dict_field.run_validation({'key': [[1, 2]]}) + + +class TestListFieldBindWithSerializers: + def test_listfield_in_serializer_with_explicit_max_depth(self): + class MySerializer(serializers.Serializer): + explicit_field = serializers.ListField( + child=serializers.IntegerField(), + max_depth=2 + ) + inherit_field = serializers.ListField( + child=serializers.IntegerField() + ) + + serializer = MySerializer(max_depth=5) + assert serializer.fields['explicit_field']._root_max_depth == 2 + assert serializer.fields['inherit_field']._root_max_depth == 5 + + def test_nested_serializers_with_listfield(self): + class InnerSerializer(serializers.Serializer): + values = serializers.ListField( + child=serializers.IntegerField(), + max_depth=3 + ) + + class OuterSerializer(serializers.Serializer): + inner = InnerSerializer(max_depth=10) + + serializer = OuterSerializer(max_depth=20) + values_field = serializer.fields['inner'].fields['values'] + assert values_field._root_max_depth == 3 + + +class TestListFieldBindEdgeCases: + def test_field_max_depth_one(self): + inner = serializers.ListField( + child=serializers.IntegerField(), + max_depth=1 + ) + outer = serializers.ListField(child=inner, max_depth=10) + assert inner._root_max_depth == 1 + output = outer.run_validation([[1, 2, 3]]) + assert output == [[1, 2, 3]] + with pytest.raises(serializers.ValidationError): + outer.run_validation([[[1]]]) + + class TestHStoreField(FieldValues): """ Values for `ListField` with CharField as child.