Skip to content

Commit 1834760

Browse files
committed
Dont change return type of Serializer.errors
We should keep `Serializer.errors`'s return type unchanged in order to maintain backward compatibility. The error codes will only be propagated to the `exception_handler` or accessible through the `Serializer._errors` private attribute.
1 parent 8c29efe commit 1834760

13 files changed

+101
-58
lines changed

rest_framework/renderers.py

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -493,21 +493,6 @@ def get_rendered_html_form(self, data, view, method, request):
493493
if hasattr(serializer, 'initial_data'):
494494
serializer.is_valid()
495495

496-
# Convert ValidationError to unicode string
497-
# This is mainly a hack to monkey patch the errors and make the form renderer happy...
498-
errors = OrderedDict()
499-
for field_name, values in serializer.errors.items():
500-
if isinstance(values, list):
501-
errors[field_name] = values
502-
continue
503-
504-
errors[field_name] = []
505-
for value in values.detail:
506-
for message in value.detail:
507-
errors[field_name].append(message)
508-
509-
serializer._errors = errors
510-
511496
form_renderer = self.form_renderer_class()
512497
return form_renderer.render(
513498
serializer.data,

rest_framework/serializers.py

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,13 @@ def is_valid(self, raise_exception=False):
219219
self._errors = {}
220220

221221
if self._errors and raise_exception:
222-
raise ValidationError(self.errors)
222+
return_errors = None
223+
if isinstance(self._errors, list):
224+
return_errors = ReturnList(self._errors, serializer=self)
225+
elif isinstance(self._errors, dict):
226+
return_errors = ReturnDict(self._errors, serializer=self)
227+
228+
raise ValidationError(return_errors)
223229

224230
return not bool(self._errors)
225231

@@ -244,12 +250,42 @@ def data(self):
244250
self._data = self.get_initial()
245251
return self._data
246252

253+
def _transform_to_legacy_errors(self, errors_to_transform):
254+
# Do not mutate `errors_to_transform` here.
255+
errors = ReturnDict(serializer=self)
256+
for field_name, values in errors_to_transform.items():
257+
if isinstance(values, list):
258+
errors[field_name] = values
259+
continue
260+
261+
if isinstance(values.detail, list):
262+
errors[field_name] = []
263+
for value in values.detail:
264+
if isinstance(value, ValidationError):
265+
errors[field_name].extend(value.detail)
266+
elif isinstance(value, list):
267+
errors[field_name].extend(value)
268+
else:
269+
errors[field_name].append(value)
270+
271+
elif isinstance(values.detail, dict):
272+
errors[field_name] = {}
273+
for sub_field_name, value in values.detail.items():
274+
errors[field_name][sub_field_name] = []
275+
for validation_error in value:
276+
errors[field_name][sub_field_name].extend(validation_error.detail)
277+
return errors
278+
247279
@property
248280
def errors(self):
249281
if not hasattr(self, '_errors'):
250282
msg = 'You must call `.is_valid()` before accessing `.errors`.'
251283
raise AssertionError(msg)
252-
return self._errors
284+
285+
if isinstance(self._errors, list):
286+
return map(self._transform_to_legacy_errors, self._errors)
287+
else:
288+
return self._transform_to_legacy_errors(self._errors)
253289

254290
@property
255291
def validated_data(self):

rest_framework/views.py

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515

1616
from rest_framework import exceptions, status
1717
from rest_framework.compat import set_rollback
18-
from rest_framework.exceptions import ValidationError, _force_text_recursive
1918
from rest_framework.request import Request
2019
from rest_framework.response import Response
2120
from rest_framework.settings import api_settings
@@ -70,18 +69,8 @@ def exception_handler(exc, context):
7069
if getattr(exc, 'wait', None):
7170
headers['Retry-After'] = '%d' % exc.wait
7271

73-
if isinstance(exc.detail, list):
74-
data = _force_text_recursive([item.detail if isinstance(item, ValidationError) else item
75-
for item in exc.detai])
76-
elif isinstance(exc.detail, dict):
77-
for field_name, e in exc.detail.items():
78-
if hasattr(e, 'detail') and isinstance(e.detail[0], ValidationError):
79-
exc.detail[field_name] = e.detail[0].detail
80-
elif isinstance(e, ValidationError):
81-
exc.detail[field_name] = e.detail
82-
else:
83-
exc.detail[field_name] = e
84-
data = exc.detail
72+
if isinstance(exc.detail, (list, dict)):
73+
data = exc.detail.serializer.errors
8574
else:
8675
data = {'detail': exc.detail}
8776

tests/test_bound_fields.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,12 @@ class ExampleSerializer(serializers.Serializer):
3838
serializer = ExampleSerializer(data={'text': 'x' * 1000, 'amount': 123})
3939
serializer.is_valid()
4040

41+
# TODO Should we add the _errors with ValidationError to the bound_field.errors to get acces to the error code?
42+
assert serializer._errors['text'].detail[0].detail == ['Ensure this field has no more than 100 characters.']
43+
assert serializer._errors['text'].detail[0].code == 'max_length'
44+
4145
assert serializer['text'].value == 'x' * 1000
42-
assert serializer['text'].errors.detail[0].detail == ['Ensure this field has no more than 100 characters.']
43-
assert serializer['text'].errors.detail[0].code == 'max_length'
46+
assert serializer['text'].errors == ['Ensure this field has no more than 100 characters.']
4447
assert serializer['text'].name == 'text'
4548
assert serializer['amount'].value is 123
4649
assert serializer['amount'].errors is None

tests/test_model_serializer.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -374,7 +374,7 @@ class Meta:
374374

375375
s = TestSerializer(data={'address': 'not an ip address'})
376376
self.assertFalse(s.is_valid())
377-
self.assertEquals(1, len(s.errors['address'].detail),
377+
self.assertEquals(1, len(s.errors['address']),
378378
'Unexpected number of validation errors: '
379379
'{0}'.format(s.errors))
380380

tests/test_relations_hyperlink.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -244,8 +244,9 @@ def test_foreign_key_update_incorrect_type(self):
244244
instance = ForeignKeySource.objects.get(pk=1)
245245
serializer = ForeignKeySourceSerializer(instance, data=data, context={'request': request})
246246
self.assertFalse(serializer.is_valid())
247-
self.assertEqual(serializer.errors['target'].detail, ['Incorrect type. Expected URL string, received int.'])
248-
self.assertEqual(serializer.errors['target'].code, 'incorrect_type')
247+
self.assertEqual(serializer.errors, {'target': ['Incorrect type. Expected URL string, received int.']})
248+
self.assertEqual(serializer._errors['target'].detail, ['Incorrect type. Expected URL string, received int.'])
249+
self.assertEqual(serializer._errors['target'].code, 'incorrect_type')
249250

250251
def test_reverse_foreign_key_update(self):
251252
data = {'url': 'http://testserver/foreignkeytarget/2/', 'name': 'target-2', 'sources': ['http://testserver/foreignkeysource/1/', 'http://testserver/foreignkeysource/3/']}
@@ -316,8 +317,9 @@ def test_foreign_key_update_with_invalid_null(self):
316317
instance = ForeignKeySource.objects.get(pk=1)
317318
serializer = ForeignKeySourceSerializer(instance, data=data, context={'request': request})
318319
self.assertFalse(serializer.is_valid())
319-
self.assertEqual(serializer.errors['target'].detail, ['This field may not be null.'])
320-
self.assertEqual(serializer.errors['target'].code, 'null')
320+
self.assertEqual(serializer.errors, {'target': ['This field may not be null.']})
321+
self.assertEqual(serializer._errors['target'].detail, ['This field may not be null.'])
322+
self.assertEqual(serializer._errors['target'].code, 'null')
321323

322324

323325
class HyperlinkedNullableForeignKeyTests(TestCase):

tests/test_relations_pk.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -235,9 +235,11 @@ def test_foreign_key_update_incorrect_type(self):
235235
instance = ForeignKeySource.objects.get(pk=1)
236236
serializer = ForeignKeySourceSerializer(instance, data=data)
237237
self.assertFalse(serializer.is_valid())
238-
self.assertEqual(serializer.errors['target'].detail,
238+
self.assertEqual(serializer.errors,
239+
{'target': ['Incorrect type. Expected pk value, received %s.' % six.text_type.__name__]})
240+
self.assertEqual(serializer._errors['target'].detail,
239241
['Incorrect type. Expected pk value, received %s.' % six.text_type.__name__])
240-
self.assertEqual(serializer.errors['target'].code, 'incorrect_type')
242+
self.assertEqual(serializer._errors['target'].code, 'incorrect_type')
241243

242244
def test_reverse_foreign_key_update(self):
243245
data = {'id': 2, 'name': 'target-2', 'sources': [1, 3]}
@@ -308,8 +310,9 @@ def test_foreign_key_update_with_invalid_null(self):
308310
instance = ForeignKeySource.objects.get(pk=1)
309311
serializer = ForeignKeySourceSerializer(instance, data=data)
310312
self.assertFalse(serializer.is_valid())
311-
self.assertEqual(serializer.errors['target'].detail, ['This field may not be null.'])
312-
self.assertEqual(serializer.errors['target'].code, 'null')
313+
self.assertEqual(serializer.errors, {'target': ['This field may not be null.']})
314+
self.assertEqual(serializer._errors['target'].detail, ['This field may not be null.'])
315+
self.assertEqual(serializer._errors['target'].code, 'null')
313316

314317
def test_foreign_key_with_unsaved(self):
315318
source = ForeignKeySource(name='source-unsaved')

tests/test_relations_slug.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -104,8 +104,9 @@ def test_foreign_key_update_incorrect_type(self):
104104
instance = ForeignKeySource.objects.get(pk=1)
105105
serializer = ForeignKeySourceSerializer(instance, data=data)
106106
self.assertFalse(serializer.is_valid())
107-
self.assertEqual(serializer.errors['target'].detail, ['Object with name=123 does not exist.'])
108-
self.assertEqual(serializer.errors['target'].code, 'does_not_exist')
107+
self.assertEqual(serializer.errors, {'target': ['Object with name=123 does not exist.']})
108+
self.assertEqual(serializer._errors['target'].detail, ['Object with name=123 does not exist.'])
109+
self.assertEqual(serializer._errors['target'].code, 'does_not_exist')
109110

110111
def test_reverse_foreign_key_update(self):
111112
data = {'id': 2, 'name': 'target-2', 'sources': ['source-1', 'source-3']}
@@ -177,8 +178,9 @@ def test_foreign_key_update_with_invalid_null(self):
177178
instance = ForeignKeySource.objects.get(pk=1)
178179
serializer = ForeignKeySourceSerializer(instance, data=data)
179180
self.assertFalse(serializer.is_valid())
180-
self.assertEqual(serializer.errors['target'].detail, ['This field may not be null.'])
181-
self.assertEqual(serializer.errors['target'].code, 'null')
181+
self.assertEqual(serializer.errors, {'target': ['This field may not be null.']})
182+
self.assertEqual(serializer._errors['target'].detail, ['This field may not be null.'])
183+
self.assertEqual(serializer._errors['target'].code, 'null')
182184

183185

184186
class SlugNullableForeignKeyTests(TestCase):

tests/test_serializer.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,9 @@ def test_invalid_serializer(self):
3232
serializer = self.Serializer(data={'char': 'abc'})
3333
assert not serializer.is_valid()
3434
assert serializer.validated_data == {}
35-
assert serializer.errors['integer'].detail == ['This field is required.']
36-
assert serializer.errors['integer'].code == 'required'
35+
assert serializer.errors == {'integer': ['This field is required.']}
36+
assert serializer._errors['integer'].detail == ['This field is required.']
37+
assert serializer._errors['integer'].code == 'required'
3738

3839
def test_partial_validation(self):
3940
serializer = self.Serializer(data={'char': 'abc'}, partial=True)

tests/test_serializer_bulk_update.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,15 @@ def test_bulk_create_errors(self):
7171
serializer = self.BookSerializer(data=data, many=True)
7272
self.assertEqual(serializer.is_valid(), False)
7373

74-
for idx, error in enumerate(serializer.errors):
74+
expected_errors = [
75+
{},
76+
{},
77+
{'id': ['A valid integer is required.']}
78+
]
79+
80+
self.assertEqual(serializer.errors, expected_errors)
81+
82+
for idx, error in enumerate(serializer._errors):
7583
if idx < 2:
7684
self.assertEqual(error, {})
7785
else:

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy