diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index ea2daffd5a..448b59b774 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -688,18 +688,17 @@ def to_internal_value(self, data): }, code='min_length') ret = [] - errors = [] + errors = {} - for item in data: + for index, item in enumerate(data): try: validated = self.run_child_validation(item) except ValidationError as exc: - errors.append(exc.detail) + errors[index] = exc.detail else: ret.append(validated) - errors.append({}) - if any(errors): + if errors: raise ValidationError(errors) return ret diff --git a/tests/test_serializer_bulk_update.py b/tests/test_serializer_bulk_update.py index 0465578bb6..c888e6aa82 100644 --- a/tests/test_serializer_bulk_update.py +++ b/tests/test_serializer_bulk_update.py @@ -65,11 +65,9 @@ def test_bulk_create_errors(self): 'author': 'Haruki Murakami' } ] - expected_errors = [ - {}, - {}, - {'id': ['A valid integer is required.']} - ] + expected_errors = { + 2: {'id': ['A valid integer is required.']} + } serializer = self.BookSerializer(data=data, many=True) assert serializer.is_valid() is False @@ -85,11 +83,7 @@ def test_invalid_list_datatype(self): assert serializer.is_valid() is False message = 'Invalid data. Expected a dictionary, but got str.' - expected_errors = [ - {'non_field_errors': [message]}, - {'non_field_errors': [message]}, - {'non_field_errors': [message]} - ] + expected_errors = {idx: {'non_field_errors': [message]} for idx in range(len(data))} assert serializer.errors == expected_errors diff --git a/tests/test_serializer_lists.py b/tests/test_serializer_lists.py index f76451a5ad..eaff6ac1fb 100644 --- a/tests/test_serializer_lists.py +++ b/tests/test_serializer_lists.py @@ -775,3 +775,80 @@ def test(self): queryset = NullableOneToOneSource.objects.all() serializer = self.serializer(queryset, many=True) assert serializer.data + + +class TestListSerializerDictErrorBehavior: + """ + Tests dict-based error structure for ListSerializer, and consistency with ListField. + + https://github.com/encode/django-rest-framework/issues/7279 + """ + + def setup_method(self): + class SampleSerializer(serializers.Serializer): + num = serializers.BooleanField() + + class ChildSerializer(serializers.Serializer): + num = serializers.BooleanField() + + class WrapperSerializer(serializers.Serializer): + list_serializer = ChildSerializer(many=True) + list_field = serializers.ListField( + child=serializers.DictField(allow_empty=False) + ) + + self.SampleSerializer = SampleSerializer + self.WrapperSerializer = WrapperSerializer + + def test_listserializer_dict_error_format(self): + + data = [ + {"num": "1"}, + {"num": "x"}, + {"num": "0"}, + {"num": "hello"}, + ] + + serializer = self.SampleSerializer(data=data, many=True) + serializer.is_valid() + + errors = serializer.errors + assert isinstance(errors, dict) + assert set(errors.keys()) == {1, 3} + + assert errors[1] == {"num": [ErrorDetail(string="Must be a valid boolean.", code="invalid")]} + assert errors[3] == {"num": [ErrorDetail(string="Must be a valid boolean.", code="invalid")]} + + def test_listserializer_and_listfield_consistency(self): + + data = { + "list_serializer": [ + {"num": "1"}, + {"num": "wrong"}, + {"num": "0"}, + {"num": ""}, + ], + "list_field": [ + {"ok": "x"}, + {}, + {"valid": "y"}, + {}, + ], + } + + serializer = self.WrapperSerializer(data=data) + serializer.is_valid() + + errors = serializer.errors + + assert isinstance(errors["list_serializer"], dict) + assert isinstance(errors["list_field"], dict) + + assert set(errors["list_serializer"].keys()) == {1, 3} + assert set(errors["list_field"].keys()) == {1, 3} + + assert errors["list_serializer"][1] == {"num": [ErrorDetail(string="Must be a valid boolean.", code="invalid")]} + assert errors["list_serializer"][3] == {"num": [ErrorDetail(string="Must be a valid boolean.", code="invalid")]} + + assert errors["list_field"][1] == [ErrorDetail(string='This dictionary may not be empty.', code='empty')] + assert errors["list_field"][3] == [ErrorDetail(string='This dictionary may not be empty.', code='empty')]