Skip to content

Commit 2bf6ee4

Browse files
committed
Introduce ValidationErrorMessage
`ValidationErrorMessage` is a string-like object that holds a code attribute. The code attribute has been removed from ValidationError to be able to maintain better backward compatibility. `ValidationErrorMessage` is abstracted in `ValidationError`'s constructor
1 parent df0d814 commit 2bf6ee4

File tree

8 files changed

+118
-66
lines changed

8 files changed

+118
-66
lines changed

rest_framework/authtoken/serializers.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,20 +20,18 @@ def validate(self, attrs):
2020
msg = _('User account is disabled.')
2121
raise serializers.ValidationError(
2222
msg,
23-
code='authorization'
24-
)
23+
code='authorization')
2524
else:
2625
msg = _('Unable to log in with provided credentials.')
2726
raise serializers.ValidationError(
2827
msg,
29-
code='authorization'
30-
)
28+
code='authorization')
29+
3130
else:
3231
msg = _('Must include "username" and "password".')
3332
raise serializers.ValidationError(
3433
msg,
35-
code='authorization'
36-
)
34+
code='authorization')
3735

3836
attrs['user'] = user
3937
return attrs

rest_framework/exceptions.py

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ def __str__(self):
6161
def build_error_from_django_validation_error(exc_info):
6262
code = getattr(exc_info, 'code', None) or 'invalid'
6363
return [
64-
ValidationError(msg, code=code)
64+
ValidationErrorMessage(msg, code=code)
6565
for msg in exc_info.messages
6666
]
6767

@@ -73,20 +73,30 @@ def build_error_from_django_validation_error(exc_info):
7373
# from rest_framework import serializers
7474
# raise serializers.ValidationError('Value was invalid')
7575

76+
class ValidationErrorMessage(six.text_type):
77+
code = None
78+
79+
def __new__(cls, string, code=None, *args, **kwargs):
80+
self = super(ValidationErrorMessage, cls).__new__(
81+
cls, string, *args, **kwargs)
82+
83+
self.code = code
84+
return self
85+
86+
7687
class ValidationError(APIException):
7788
status_code = status.HTTP_400_BAD_REQUEST
7889

7990
def __init__(self, detail, code=None):
91+
# If code is there, this means we are dealing with a message.
92+
if code and not isinstance(detail, ValidationErrorMessage):
93+
detail = ValidationErrorMessage(detail, code=code)
94+
8095
# For validation errors the 'detail' key is always required.
8196
# The details should always be coerced to a list if not already.
8297
if not isinstance(detail, dict) and not isinstance(detail, list):
8398
detail = [detail]
84-
elif isinstance(detail, dict) or (detail and isinstance(detail[0], ValidationError)):
85-
assert code is None, (
86-
'The `code` argument must not be set for compound errors.')
87-
88-
self.detail = detail
89-
self.code = code
99+
self.detail = _force_text_recursive(detail)
90100

91101
def __str__(self):
92102
return six.text_type(self.detail)

rest_framework/fields.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -509,7 +509,7 @@ def run_validators(self, value):
509509
# attempting to accumulate a list of errors.
510510
if isinstance(exc.detail, dict):
511511
raise
512-
errors.append(ValidationError(exc.detail, code=exc.code))
512+
errors.extend(exc.detail)
513513
except DjangoValidationError as exc:
514514
errors.extend(
515515
build_error_from_django_validation_error(exc)

rest_framework/response.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ def __init__(self, data=None, status=None,
3838
'`.error`. representation.'
3939
)
4040
raise AssertionError(msg)
41+
4142
self.data = data
4243
self.template_name = template_name
4344
self.exception = exception

rest_framework/serializers.py

Lines changed: 10 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
from rest_framework import exceptions
2626
from rest_framework.compat import JSONField as ModelJSONField
2727
from rest_framework.compat import postgres_fields, unicode_to_repr
28+
from rest_framework.exceptions import ValidationErrorMessage
2829
from rest_framework.utils import model_meta
2930
from rest_framework.utils.field_mapping import (
3031
ClassLookupDict, get_field_kwargs, get_nested_relation_kwargs,
@@ -220,14 +221,7 @@ def is_valid(self, raise_exception=False):
220221
self._errors = {}
221222

222223
if self._errors and raise_exception:
223-
return_errors = None
224-
if isinstance(self._errors, list):
225-
return_errors = ReturnList(self._errors, serializer=self)
226-
elif isinstance(self._errors, dict):
227-
return_errors = ReturnDict(self._errors, serializer=self)
228-
229-
raise ValidationError(return_errors)
230-
224+
raise ValidationError(self.errors)
231225
return not bool(self._errors)
232226

233227
@property
@@ -251,42 +245,12 @@ def data(self):
251245
self._data = self.get_initial()
252246
return self._data
253247

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

291255
@property
292256
def validated_data(self):
@@ -461,7 +425,7 @@ def to_internal_value(self, data):
461425
message = self.error_messages['invalid'].format(
462426
datatype=type(data).__name__
463427
)
464-
error = ValidationError(message, code='invalid')
428+
error = ValidationErrorMessage(message, code='invalid')
465429
raise ValidationError({
466430
api_settings.NON_FIELD_ERRORS_KEY: [error]
467431
})
@@ -478,7 +442,7 @@ def to_internal_value(self, data):
478442
if validate_method is not None:
479443
validated_value = validate_method(validated_value)
480444
except ValidationError as exc:
481-
errors[field.field_name] = exc
445+
errors[field.field_name] = exc.detail
482446
except DjangoValidationError as exc:
483447
errors[field.field_name] = (
484448
exceptions.build_error_from_django_validation_error(exc)
@@ -621,7 +585,7 @@ def to_internal_value(self, data):
621585
message = self.error_messages['not_a_list'].format(
622586
input_type=type(data).__name__
623587
)
624-
error = ValidationError(
588+
error = ValidationErrorMessage(
625589
message,
626590
code='not_a_list'
627591
)
@@ -630,9 +594,11 @@ def to_internal_value(self, data):
630594
})
631595

632596
if not self.allow_empty and len(data) == 0:
633-
message = self.error_messages['empty']
597+
message = ValidationErrorMessage(
598+
self.error_messages['empty'],
599+
code='empty_not_allowed')
634600
raise ValidationError({
635-
api_settings.NON_FIELD_ERRORS_KEY: [ValidationError(message, code='empty_not_allowed')]
601+
api_settings.NON_FIELD_ERRORS_KEY: [message]
636602
})
637603

638604
ret = []

rest_framework/validators.py

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from django.utils.translation import ugettext_lazy as _
1313

1414
from rest_framework.compat import unicode_to_repr
15-
from rest_framework.exceptions import ValidationError
15+
from rest_framework.exceptions import ValidationError, ValidationErrorMessage
1616
from rest_framework.utils.representation import smart_repr
1717

1818

@@ -120,9 +120,10 @@ def enforce_required_fields(self, attrs):
120120
return
121121

122122
missing = {
123-
field_name: ValidationError(
123+
field_name: ValidationErrorMessage(
124124
self.missing_message,
125125
code='required')
126+
126127
for field_name in self.fields
127128
if field_name not in attrs
128129
}
@@ -168,8 +169,9 @@ def __call__(self, attrs):
168169
]
169170
if None not in checked_values and qs_exists(queryset):
170171
field_names = ', '.join(self.fields)
171-
raise ValidationError(self.message.format(field_names=field_names),
172-
code='unique')
172+
raise ValidationError(
173+
self.message.format(field_names=field_names),
174+
code='unique')
173175

174176
def __repr__(self):
175177
return unicode_to_repr('<%s(queryset=%s, fields=%s)>' % (
@@ -207,7 +209,7 @@ def enforce_required_fields(self, attrs):
207209
'required' state on the fields they are applied to.
208210
"""
209211
missing = {
210-
field_name: ValidationError(
212+
field_name: ValidationErrorMessage(
211213
self.missing_message,
212214
code='required')
213215
for field_name in [self.field, self.date_field]
@@ -235,8 +237,9 @@ def __call__(self, attrs):
235237
queryset = self.exclude_current_instance(attrs, queryset)
236238
if qs_exists(queryset):
237239
message = self.message.format(date_field=self.date_field)
238-
error = ValidationError(message, code='unique')
239-
raise ValidationError({self.field: error})
240+
raise ValidationError({
241+
self.field: ValidationErrorMessage(message, code='unique'),
242+
})
240243

241244
def __repr__(self):
242245
return unicode_to_repr('<%s(queryset=%s, field=%s, date_field=%s)>' % (

rest_framework/views.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ def exception_handler(exc, context):
7171
headers['Retry-After'] = '%d' % exc.wait
7272

7373
if isinstance(exc.detail, (list, dict)):
74-
data = exc.detail.serializer.errors
74+
data = exc.detail
7575
else:
7676
data = {'detail': exc.detail}
7777

tests/test_validation_error.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
from django.test import TestCase
2+
3+
from rest_framework import serializers, status
4+
from rest_framework.decorators import api_view
5+
from rest_framework.response import Response
6+
from rest_framework.settings import api_settings
7+
from rest_framework.test import APIRequestFactory
8+
from rest_framework.views import APIView
9+
10+
factory = APIRequestFactory()
11+
12+
13+
class ExampleSerializer(serializers.Serializer):
14+
char = serializers.CharField()
15+
integer = serializers.IntegerField()
16+
17+
18+
class ErrorView(APIView):
19+
def get(self, request, *args, **kwargs):
20+
ExampleSerializer(data={}).is_valid(raise_exception=True)
21+
22+
23+
@api_view(['GET'])
24+
def error_view(request):
25+
ExampleSerializer(data={}).is_valid(raise_exception=True)
26+
27+
28+
class TestValidationErrorWithCode(TestCase):
29+
def setUp(self):
30+
self.DEFAULT_HANDLER = api_settings.EXCEPTION_HANDLER
31+
32+
def exception_handler(exc, request):
33+
return_errors = {}
34+
for field_name, errors in exc.detail.items():
35+
return_errors[field_name] = []
36+
for error in errors:
37+
return_errors[field_name].append({
38+
'code': error.code,
39+
'message': error
40+
})
41+
42+
return Response(return_errors, status=status.HTTP_400_BAD_REQUEST)
43+
44+
api_settings.EXCEPTION_HANDLER = exception_handler
45+
46+
self.expected_response_data = {
47+
'char': [{
48+
'message': 'This field is required.',
49+
'code': 'required',
50+
}],
51+
'integer': [{
52+
'message': 'This field is required.',
53+
'code': 'required'
54+
}],
55+
}
56+
57+
def tearDown(self):
58+
api_settings.EXCEPTION_HANDLER = self.DEFAULT_HANDLER
59+
60+
def test_class_based_view_exception_handler(self):
61+
view = ErrorView.as_view()
62+
63+
request = factory.get('/', content_type='application/json')
64+
response = view(request)
65+
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
66+
self.assertEqual(response.data, self.expected_response_data)
67+
68+
def test_function_based_view_exception_handler(self):
69+
view = error_view
70+
71+
request = factory.get('/', content_type='application/json')
72+
response = view(request)
73+
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
74+
self.assertEqual(response.data, self.expected_response_data)

0 commit comments

Comments
 (0)