Repository: encode/django-rest-framework

Issue:
3.15 regression: ListSerializer ValidationErrors silently changed return type
- [`ListSerializer.errors` has type `ReturnList`](https://github.com/encode/django-rest-framework/blob/77ef27f18fc7c11e1d2e5fd4aaa8acc51cda6792/rest_framework/serializers.py#L808).
- When raising, #8863 [silently casts this into a list](https://github.com/encode/django-rest-framework/pull/8863/files#diff-2b352f708f44b59b1c568d234dafdfc405760d55eaee4fed1ee9fbd140d49886R163).
- As a result, the exceptions `.detail` attribute is no longer of type `ReturnList`.
- As a result, the `.detail.serializer` attribute is no longer available. [That attribute is the whole point of the existence of `ReturnList`.](https://github.com/encode/django-rest-framework/blob/77ef27f18fc7c11e1d2e5fd4aaa8acc51cda6792/rest_framework/utils/serializer_helpers.py#L49-L54)

This is a breaking change in a public API and should be reverted.


Discussion:

Resolving patch:
diff --git a/rest_framework/exceptions.py b/rest_framework/exceptions.py
--- a/rest_framework/exceptions.py
+++ b/rest_framework/exceptions.py
@@ -144,30 +144,17 @@ class ValidationError(APIException):
     status_code = status.HTTP_400_BAD_REQUEST
     default_detail = _('Invalid input.')
     default_code = 'invalid'
-    default_params = {}
 
-    def __init__(self, detail=None, code=None, params=None):
+    def __init__(self, detail=None, code=None):
         if detail is None:
             detail = self.default_detail
         if code is None:
             code = self.default_code
-        if params is None:
-            params = self.default_params
 
         # For validation failures, we may collect many errors together,
         # so the details should always be coerced to a list if not already.
-        if isinstance(detail, str):
-            detail = [detail % params]
-        elif isinstance(detail, ValidationError):
-            detail = detail.detail
-        elif isinstance(detail, (list, tuple)):
-            final_detail = []
-            for detail_item in detail:
-                if isinstance(detail_item, ValidationError):
-                    final_detail += detail_item.detail
-                else:
-                    final_detail += [detail_item % params if isinstance(detail_item, str) else detail_item]
-            detail = final_detail
+        if isinstance(detail, tuple):
+            detail = list(detail)
         elif not isinstance(detail, dict) and not isinstance(detail, list):
             detail = [detail]
 


Test patch:
diff --git a/tests/test_validation_error.py b/tests/test_validation_error.py
--- a/tests/test_validation_error.py
+++ b/tests/test_validation_error.py
@@ -109,89 +109,3 @@ def test_validation_error_details(self):
         assert len(error.detail) == 2
         assert str(error.detail[0]) == 'message1'
         assert str(error.detail[1]) == 'message2'
-
-
-class TestValidationErrorWithDjangoStyle(TestCase):
-    def test_validation_error_details(self):
-        error = ValidationError('Invalid value: %(value)s', params={'value': '42'})
-        assert str(error.detail[0]) == 'Invalid value: 42'
-
-    def test_validation_error_details_tuple(self):
-        error = ValidationError(
-            detail=('Invalid value: %(value1)s', 'Invalid value: %(value2)s'),
-            params={'value1': '42', 'value2': '43'},
-        )
-        assert isinstance(error.detail, list)
-        assert len(error.detail) == 2
-        assert str(error.detail[0]) == 'Invalid value: 42'
-        assert str(error.detail[1]) == 'Invalid value: 43'
-
-    def test_validation_error_details_list(self):
-        error = ValidationError(
-            detail=['Invalid value: %(value1)s', 'Invalid value: %(value2)s', ],
-            params={'value1': '42', 'value2': '43'}
-        )
-        assert isinstance(error.detail, list)
-        assert len(error.detail) == 2
-        assert str(error.detail[0]) == 'Invalid value: 42'
-        assert str(error.detail[1]) == 'Invalid value: 43'
-
-    def test_validation_error_details_validation_errors(self):
-        error = ValidationError(
-            detail=ValidationError(
-                detail='Invalid value: %(value1)s',
-                params={'value1': '42'},
-            ),
-        )
-        assert isinstance(error.detail, list)
-        assert len(error.detail) == 1
-        assert str(error.detail[0]) == 'Invalid value: 42'
-
-    def test_validation_error_details_validation_errors_list(self):
-        error = ValidationError(
-            detail=[
-                ValidationError(
-                    detail='Invalid value: %(value1)s',
-                    params={'value1': '42'},
-                ),
-                ValidationError(
-                    detail='Invalid value: %(value2)s',
-                    params={'value2': '43'},
-                ),
-                'Invalid value: %(value3)s'
-            ],
-            params={'value3': '44'}
-        )
-        assert isinstance(error.detail, list)
-        assert len(error.detail) == 3
-        assert str(error.detail[0]) == 'Invalid value: 42'
-        assert str(error.detail[1]) == 'Invalid value: 43'
-        assert str(error.detail[2]) == 'Invalid value: 44'
-
-    def test_validation_error_details_validation_errors_nested_list(self):
-        error = ValidationError(
-            detail=[
-                ValidationError(
-                    detail='Invalid value: %(value1)s',
-                    params={'value1': '42'},
-                ),
-                ValidationError(
-                    detail=[
-                        'Invalid value: %(value2)s',
-                        ValidationError(
-                            detail='Invalid value: %(value3)s',
-                            params={'value3': '44'},
-                        )
-                    ],
-                    params={'value2': '43'},
-                ),
-                'Invalid value: %(value4)s'
-            ],
-            params={'value4': '45'}
-        )
-        assert isinstance(error.detail, list)
-        assert len(error.detail) == 4
-        assert str(error.detail[0]) == 'Invalid value: 42'
-        assert str(error.detail[1]) == 'Invalid value: 43'
-        assert str(error.detail[2]) == 'Invalid value: 44'
-        assert str(error.detail[3]) == 'Invalid value: 45'