diff --git a/docs/api-guide/serializers.md b/docs/api-guide/serializers.md index 7259cb6676..6a82f8fe09 100644 --- a/docs/api-guide/serializers.md +++ b/docs/api-guide/serializers.md @@ -159,7 +159,9 @@ When deserializing data, you always need to call `is_valid()` before attempting Each key in the dictionary will be the field name, and the values will be lists of strings of any error messages corresponding to that field. The `non_field_errors` key may also be present, and will list any general validation errors. The name of the `non_field_errors` key may be customized using the `NON_FIELD_ERRORS_KEY` REST framework setting. -When deserializing a list of items, errors will be returned as a list of dictionaries representing each of the deserialized items. +When deserializing a list of items, errors are returned as a dictionary keyed by the indexes of invalid items. Valid items are omitted from the dictionary. + +To temporarily use the list-based format from versions before REST framework 3.18, set `LIST_SERIALIZER_ERRORS_AS_DICT` to `False`. This format includes an empty dictionary for each valid item and is deprecated. It will be removed in REST framework 3.20. #### Raising an exception on invalid data diff --git a/docs/api-guide/settings.md b/docs/api-guide/settings.md index f0fc5a455a..b3f9d16d71 100644 --- a/docs/api-guide/settings.md +++ b/docs/api-guide/settings.md @@ -462,6 +462,16 @@ A string representing the key that should be used for serializer errors that do Default: `'non_field_errors'` +#### LIST_SERIALIZER_ERRORS_AS_DICT + +Controls the format of per-item validation errors produced by `ListSerializer`, including serializers instantiated with `many=True`. + +When set to `True`, errors are returned as a dictionary keyed by the indexes of invalid items. Valid items are omitted from the dictionary. This format was introduced in REST framework 3.18.0. + +When set to `False`, errors are returned in the list-based format used before REST framework 3.18, with one entry for each input item and an empty dictionary for each valid item. This format is deprecated and will be removed in REST framework 3.20. + +Default: `True` + #### URL_FIELD_NAME A string representing the key that should be used for the URL fields generated by `HyperlinkedModelSerializer`. diff --git a/rest_framework/deprecation.py b/rest_framework/deprecation.py new file mode 100644 index 0000000000..ff03c65bf4 --- /dev/null +++ b/rest_framework/deprecation.py @@ -0,0 +1,2 @@ +class RemovedInDRF320Warning(PendingDeprecationWarning): + pass diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index a8aa3df68f..fc8e83c768 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -15,6 +15,7 @@ import copy import inspect import traceback +import warnings from collections import defaultdict from collections.abc import Mapping @@ -27,6 +28,7 @@ from django.utils.translation import gettext_lazy as _ from rest_framework.compat import postgres_fields +from rest_framework.deprecation import RemovedInDRF320Warning from rest_framework.exceptions import ErrorDetail, ValidationError from rest_framework.fields import get_error_detail from rest_framework.settings import api_settings @@ -709,6 +711,16 @@ def to_internal_value(self, data): ret.append(validated) if errors: + if not api_settings.LIST_SERIALIZER_ERRORS_AS_DICT: + warnings.warn( + 'The list-based error format for `ListSerializer` is ' + 'deprecated and will be removed in DRF 3.20. Set ' + '`REST_FRAMEWORK["LIST_SERIALIZER_ERRORS_AS_DICT"]` to ' + '`True` to use the dictionary-based error format.', + RemovedInDRF320Warning, + stacklevel=4, + ) + errors = [errors.get(index, {}) for index in range(len(data))] raise ValidationError(errors) return ret diff --git a/rest_framework/settings.py b/rest_framework/settings.py index c6cc97c53d..42b1156e3a 100644 --- a/rest_framework/settings.py +++ b/rest_framework/settings.py @@ -86,6 +86,7 @@ # Exception handling 'EXCEPTION_HANDLER': 'rest_framework.views.exception_handler', 'NON_FIELD_ERRORS_KEY': 'non_field_errors', + 'LIST_SERIALIZER_ERRORS_AS_DICT': True, # Testing 'TEST_REQUEST_RENDERER_CLASSES': [ diff --git a/tests/test_serializer_lists.py b/tests/test_serializer_lists.py index 0fdfcdb87b..cbbdeaffd1 100644 --- a/tests/test_serializer_lists.py +++ b/tests/test_serializer_lists.py @@ -1,8 +1,12 @@ +import warnings + import pytest from django.http import QueryDict +from django.test import override_settings from django.utils.datastructures import MultiValueDict from rest_framework import serializers +from rest_framework.deprecation import RemovedInDRF320Warning from rest_framework.exceptions import ErrorDetail from tests.models import ( CustomManagerModel, NullableOneToOneSource, OneToOneTarget @@ -885,9 +889,9 @@ def test(self): assert serializer.data -class TestListSerializerDictErrorBehavior: +class TestListSerializerErrorBehavior: """ - Tests dict-based error structure for ListSerializer, and consistency with ListField. + Tests both ListSerializer error formats and consistency with ListField. https://github.com/encode/django-rest-framework/issues/7279 """ @@ -908,8 +912,7 @@ class WrapperSerializer(serializers.Serializer): self.SampleSerializer = SampleSerializer self.WrapperSerializer = WrapperSerializer - def test_listserializer_dict_error_format(self): - + def test_listserializer_dict_error_format_by_default(self): data = [ {"num": "1"}, {"num": "x"}, @@ -918,15 +921,39 @@ def test_listserializer_dict_error_format(self): ] serializer = self.SampleSerializer(data=data, many=True) - serializer.is_valid() + with warnings.catch_warnings(): + warnings.simplefilter('error', RemovedInDRF320Warning) + assert not 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")]} + @override_settings(REST_FRAMEWORK={'LIST_SERIALIZER_ERRORS_AS_DICT': False}) + def test_listserializer_explicit_legacy_error_format(self): + data = [ + {"num": "1"}, + {"num": "wrong"}, + {"num": "0"}, + ] + + serializer = self.SampleSerializer(data=data, many=True) + with pytest.warns( + RemovedInDRF320Warning, + match='LIST_SERIALIZER_ERRORS_AS_DICT' + ) as warning: + assert not serializer.is_valid() + + assert isinstance(serializer.errors, list) + assert serializer.errors == [ + {}, + {"num": [ErrorDetail(string="Must be a valid boolean.", code="invalid")]}, + {}, + ] + assert warning[0].filename == __file__ + def test_listserializer_and_listfield_consistency(self): data = { @@ -945,7 +972,7 @@ def test_listserializer_and_listfield_consistency(self): } serializer = self.WrapperSerializer(data=data) - serializer.is_valid() + assert not serializer.is_valid() errors = serializer.errors