Skip to content

Commit 42f4c55

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. What this means is that `ValidationError` can accept either a regular string or a `ValidationErrorMessage` for its `detail` attribute.
1 parent c7351b3 commit 42f4c55

File tree

8 files changed

+127
-70
lines changed

8 files changed

+127
-70
lines changed

rest_framework/authtoken/serializers.py

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from django.utils.translation import ugettext_lazy as _
33

44
from rest_framework import serializers
5+
from rest_framework.exceptions import ValidationErrorMessage
56

67

78
class AuthTokenSerializer(serializers.Serializer):
@@ -19,20 +20,23 @@ def validate(self, attrs):
1920
if not user.is_active:
2021
msg = _('User account is disabled.')
2122
raise serializers.ValidationError(
22-
msg,
23-
code='authorization'
23+
ValidationErrorMessage(
24+
msg,
25+
code='authorization')
2426
)
2527
else:
2628
msg = _('Unable to log in with provided credentials.')
2729
raise serializers.ValidationError(
28-
msg,
29-
code='authorization'
30+
ValidationErrorMessage(
31+
msg,
32+
code='authorization')
3033
)
3134
else:
3235
msg = _('Must include "username" and "password".')
3336
raise serializers.ValidationError(
34-
msg,
35-
code='authorization'
37+
ValidationErrorMessage(
38+
msg,
39+
code='authorization')
3640
)
3741

3842
attrs['user'] = user

rest_framework/exceptions.py

Lines changed: 14 additions & 8 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,26 @@ 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

79-
def __init__(self, detail, code=None):
90+
def __init__(self, detail):
8091
# For validation errors the 'detail' key is always required.
8192
# The details should always be coerced to a list if not already.
8293
if not isinstance(detail, dict) and not isinstance(detail, list):
8394
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
95+
self.detail = _force_text_recursive(detail)
9096

9197
def __str__(self):
9298
return six.text_type(self.detail)

rest_framework/fields.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@
3232
unicode_to_repr
3333
)
3434
from rest_framework.exceptions import (
35-
ValidationError, build_error_from_django_validation_error
35+
ValidationError, ValidationErrorMessage,
36+
build_error_from_django_validation_error
3637
)
3738
from rest_framework.settings import api_settings
3839
from rest_framework.utils import html, humanize_datetime, representation
@@ -503,7 +504,7 @@ def run_validators(self, value):
503504
# attempting to accumulate a list of errors.
504505
if isinstance(exc.detail, dict):
505506
raise
506-
errors.append(ValidationError(exc.detail, code=exc.code))
507+
errors.extend(exc.detail)
507508
except DjangoValidationError as exc:
508509
errors.extend(
509510
build_error_from_django_validation_error(exc)
@@ -545,7 +546,7 @@ def fail(self, key, **kwargs):
545546
msg = MISSING_ERROR_MESSAGE.format(class_name=class_name, key=key)
546547
raise AssertionError(msg)
547548
message_string = msg.format(**kwargs)
548-
raise ValidationError(message_string, code=key)
549+
raise ValidationError(ValidationErrorMessage(message_string, code=key))
549550

550551
@cached_property
551552
def root(self):

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: 9 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -219,14 +219,7 @@ def is_valid(self, raise_exception=False):
219219
self._errors = {}
220220

221221
if self._errors and raise_exception:
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)
229-
222+
raise ValidationError(self.errors)
230223
return not bool(self._errors)
231224

232225
@property
@@ -250,42 +243,12 @@ def data(self):
250243
self._data = self.get_initial()
251244
return self._data
252245

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-
279246
@property
280247
def errors(self):
281248
if not hasattr(self, '_errors'):
282249
msg = 'You must call `.is_valid()` before accessing `.errors`.'
283250
raise AssertionError(msg)
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)
251+
return self._errors
289252

290253
@property
291254
def validated_data(self):
@@ -460,7 +423,7 @@ def to_internal_value(self, data):
460423
message = self.error_messages['invalid'].format(
461424
datatype=type(data).__name__
462425
)
463-
error = ValidationError(message, code='invalid')
426+
error = ValidationErrorMessage(message, code='invalid')
464427
raise ValidationError({
465428
api_settings.NON_FIELD_ERRORS_KEY: [error]
466429
})
@@ -477,7 +440,7 @@ def to_internal_value(self, data):
477440
if validate_method is not None:
478441
validated_value = validate_method(validated_value)
479442
except ValidationError as exc:
480-
errors[field.field_name] = exc
443+
errors[field.field_name] = exc.detail
481444
except DjangoValidationError as exc:
482445
errors[field.field_name] = (
483446
exceptions.build_error_from_django_validation_error(exc)
@@ -616,7 +579,7 @@ def to_internal_value(self, data):
616579
message = self.error_messages['not_a_list'].format(
617580
input_type=type(data).__name__
618581
)
619-
error = ValidationError(
582+
error = ValidationErrorMessage(
620583
message,
621584
code='not_a_list'
622585
)
@@ -625,9 +588,11 @@ def to_internal_value(self, data):
625588
})
626589

627590
if not self.allow_empty and len(data) == 0:
628-
message = self.error_messages['empty']
591+
message = ValidationErrorMessage(
592+
self.error_messages['empty'],
593+
code='empty_not_allowed')
629594
raise ValidationError({
630-
api_settings.NON_FIELD_ERRORS_KEY: [ValidationError(message, code='empty_not_allowed')]
595+
api_settings.NON_FIELD_ERRORS_KEY: [message]
631596
})
632597

633598
ret = []

rest_framework/validators.py

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from django.utils.translation import ugettext_lazy as _
1212

1313
from rest_framework.compat import unicode_to_repr
14-
from rest_framework.exceptions import ValidationError
14+
from rest_framework.exceptions import ValidationError, ValidationErrorMessage
1515
from rest_framework.utils.representation import smart_repr
1616

1717

@@ -60,7 +60,8 @@ def __call__(self, value):
6060
queryset = self.filter_queryset(value, queryset)
6161
queryset = self.exclude_current_instance(queryset)
6262
if queryset.exists():
63-
raise ValidationError(self.message, code='unique')
63+
raise ValidationError(ValidationErrorMessage(self.message,
64+
code='unique'))
6465

6566
def __repr__(self):
6667
return unicode_to_repr('<%s(queryset=%s)>' % (
@@ -101,9 +102,10 @@ def enforce_required_fields(self, attrs):
101102
return
102103

103104
missing = {
104-
field_name: ValidationError(
105+
field_name: ValidationErrorMessage(
105106
self.missing_message,
106107
code='required')
108+
107109
for field_name in self.fields
108110
if field_name not in attrs
109111
}
@@ -149,8 +151,11 @@ def __call__(self, attrs):
149151
]
150152
if None not in checked_values and queryset.exists():
151153
field_names = ', '.join(self.fields)
152-
raise ValidationError(self.message.format(field_names=field_names),
153-
code='unique')
154+
raise ValidationError(
155+
ValidationErrorMessage(
156+
self.message.format(field_names=field_names),
157+
code='unique')
158+
)
154159

155160
def __repr__(self):
156161
return unicode_to_repr('<%s(queryset=%s, fields=%s)>' % (
@@ -188,7 +193,7 @@ def enforce_required_fields(self, attrs):
188193
'required' state on the fields they are applied to.
189194
"""
190195
missing = {
191-
field_name: ValidationError(
196+
field_name: ValidationErrorMessage(
192197
self.missing_message,
193198
code='required')
194199
for field_name in [self.field, self.date_field]
@@ -216,8 +221,9 @@ def __call__(self, attrs):
216221
queryset = self.exclude_current_instance(attrs, queryset)
217222
if queryset.exists():
218223
message = self.message.format(date_field=self.date_field)
219-
error = ValidationError(message, code='unique')
220-
raise ValidationError({self.field: error})
224+
raise ValidationError({
225+
self.field: ValidationErrorMessage(message, code='unique'),
226+
})
221227

222228
def __repr__(self):
223229
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
@@ -70,7 +70,7 @@ def exception_handler(exc, context):
7070
headers['Retry-After'] = '%d' % exc.wait
7171

7272
if isinstance(exc.detail, (list, dict)):
73-
data = exc.detail.serializer.errors
73+
data = exc.detail
7474
else:
7575
data = {'detail': exc.detail}
7676

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)
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