Skip to content
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
4 changes: 3 additions & 1 deletion docs/api-guide/serializers.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
10 changes: 10 additions & 0 deletions docs/api-guide/settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down
2 changes: 2 additions & 0 deletions rest_framework/deprecation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
class RemovedInDRF320Warning(PendingDeprecationWarning):
pass
12 changes: 12 additions & 0 deletions rest_framework/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import copy
import inspect
import traceback
import warnings
from collections import defaultdict
from collections.abc import Mapping

Expand All @@ -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
Expand Down Expand Up @@ -709,6 +711,16 @@ def to_internal_value(self, data):
ret.append(validated)

if errors:
if not api_settings.LIST_SERIALIZER_ERRORS_AS_DICT:
Comment thread
browniebroke marked this conversation as resolved.
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
Expand Down
1 change: 1 addition & 0 deletions rest_framework/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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': [
Expand Down
41 changes: 34 additions & 7 deletions tests/test_serializer_lists.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
"""
Expand All @@ -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"},
Expand All @@ -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 = {
Expand All @@ -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

Expand Down