mirror of
https://github.com/encode/django-rest-framework.git
synced 2025-08-08 14:24:48 +03:00
Introduce error code for validation errors.
This patch is meant to fix #3111, regarding comments made to #3137 and #3169. The `ValidationError` will now contain a `code` attribute. Before this patch, `ValidationError.detail` only contained a `dict` with values equal to a `list` of string error messages or directly a `list` containing string error messages. Now, the string error messages are replaced with `ValidationError`. This means that, depending on the case, you will not only get a string back but a all object containing both the error message and the error code, respectively `ValidationError.detail` and `ValidationError.code`. It is important to note that the `code` attribute is not relevant when the `ValidationError` represents a combination of errors and hence is `None` in such cases. The main benefit of this change is that the error message and error code are now accessible the custom exception handler and can be used to format the error response. An custom exception handler example is available in the `TestValidationErrorWithCode` test.
This commit is contained in:
parent
e83175323b
commit
8c29efef48
|
@ -18,13 +18,22 @@ class AuthTokenSerializer(serializers.Serializer):
|
|||
if user:
|
||||
if not user.is_active:
|
||||
msg = _('User account is disabled.')
|
||||
raise serializers.ValidationError(msg)
|
||||
raise serializers.ValidationError(
|
||||
msg,
|
||||
code='authorization'
|
||||
)
|
||||
else:
|
||||
msg = _('Unable to log in with provided credentials.')
|
||||
raise serializers.ValidationError(msg)
|
||||
raise serializers.ValidationError(
|
||||
msg,
|
||||
code='authorization'
|
||||
)
|
||||
else:
|
||||
msg = _('Must include "username" and "password".')
|
||||
raise serializers.ValidationError(msg)
|
||||
raise serializers.ValidationError(
|
||||
msg,
|
||||
code='authorization'
|
||||
)
|
||||
|
||||
attrs['user'] = user
|
||||
return attrs
|
||||
|
|
|
@ -58,6 +58,14 @@ class APIException(Exception):
|
|||
return self.detail
|
||||
|
||||
|
||||
def build_error_from_django_validation_error(exc_info):
|
||||
code = getattr(exc_info, 'code', None) or 'invalid'
|
||||
return [
|
||||
ValidationError(msg, code=code)
|
||||
for msg in exc_info.messages
|
||||
]
|
||||
|
||||
|
||||
# The recommended style for using `ValidationError` is to keep it namespaced
|
||||
# under `serializers`, in order to minimize potential confusion with Django's
|
||||
# built in `ValidationError`. For example:
|
||||
|
@ -68,12 +76,17 @@ class APIException(Exception):
|
|||
class ValidationError(APIException):
|
||||
status_code = status.HTTP_400_BAD_REQUEST
|
||||
|
||||
def __init__(self, detail):
|
||||
def __init__(self, detail, code=None):
|
||||
# For validation errors the 'detail' key is always required.
|
||||
# The details should always be coerced to a list if not already.
|
||||
if not isinstance(detail, dict) and not isinstance(detail, list):
|
||||
detail = [detail]
|
||||
self.detail = _force_text_recursive(detail)
|
||||
elif isinstance(detail, dict) or (detail and isinstance(detail[0], ValidationError)):
|
||||
assert code is None, (
|
||||
'The `code` argument must not be set for compound errors.')
|
||||
|
||||
self.detail = detail
|
||||
self.code = code
|
||||
|
||||
def __str__(self):
|
||||
return six.text_type(self.detail)
|
||||
|
|
|
@ -31,7 +31,9 @@ from rest_framework.compat import (
|
|||
MinValueValidator, duration_string, parse_duration, unicode_repr,
|
||||
unicode_to_repr
|
||||
)
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from rest_framework.exceptions import (
|
||||
ValidationError, build_error_from_django_validation_error
|
||||
)
|
||||
from rest_framework.settings import api_settings
|
||||
from rest_framework.utils import html, humanize_datetime, representation
|
||||
|
||||
|
@ -501,9 +503,11 @@ class Field(object):
|
|||
# attempting to accumulate a list of errors.
|
||||
if isinstance(exc.detail, dict):
|
||||
raise
|
||||
errors.extend(exc.detail)
|
||||
errors.append(ValidationError(exc.detail, code=exc.code))
|
||||
except DjangoValidationError as exc:
|
||||
errors.extend(exc.messages)
|
||||
errors.extend(
|
||||
build_error_from_django_validation_error(exc)
|
||||
)
|
||||
if errors:
|
||||
raise ValidationError(errors)
|
||||
|
||||
|
@ -541,7 +545,7 @@ class Field(object):
|
|||
msg = MISSING_ERROR_MESSAGE.format(class_name=class_name, key=key)
|
||||
raise AssertionError(msg)
|
||||
message_string = msg.format(**kwargs)
|
||||
raise ValidationError(message_string)
|
||||
raise ValidationError(message_string, code=key)
|
||||
|
||||
@cached_property
|
||||
def root(self):
|
||||
|
|
|
@ -493,6 +493,21 @@ class BrowsableAPIRenderer(BaseRenderer):
|
|||
if hasattr(serializer, 'initial_data'):
|
||||
serializer.is_valid()
|
||||
|
||||
# Convert ValidationError to unicode string
|
||||
# This is mainly a hack to monkey patch the errors and make the form renderer happy...
|
||||
errors = OrderedDict()
|
||||
for field_name, values in serializer.errors.items():
|
||||
if isinstance(values, list):
|
||||
errors[field_name] = values
|
||||
continue
|
||||
|
||||
errors[field_name] = []
|
||||
for value in values.detail:
|
||||
for message in value.detail:
|
||||
errors[field_name].append(message)
|
||||
|
||||
serializer._errors = errors
|
||||
|
||||
form_renderer = self.form_renderer_class()
|
||||
return form_renderer.render(
|
||||
serializer.data,
|
||||
|
|
|
@ -38,7 +38,6 @@ class Response(SimpleTemplateResponse):
|
|||
'`.error`. representation.'
|
||||
)
|
||||
raise AssertionError(msg)
|
||||
|
||||
self.data = data
|
||||
self.template_name = template_name
|
||||
self.exception = exception
|
||||
|
|
|
@ -20,6 +20,7 @@ from django.db.models.fields import FieldDoesNotExist
|
|||
from django.utils.functional import cached_property
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from rest_framework import exceptions
|
||||
from rest_framework.compat import DurationField as ModelDurationField
|
||||
from rest_framework.compat import JSONField as ModelJSONField
|
||||
from rest_framework.compat import postgres_fields, unicode_to_repr
|
||||
|
@ -300,7 +301,8 @@ def get_validation_error_detail(exc):
|
|||
# exception class as well for simpler compat.
|
||||
# Eg. Calling Model.clean() explicitly inside Serializer.validate()
|
||||
return {
|
||||
api_settings.NON_FIELD_ERRORS_KEY: list(exc.messages)
|
||||
api_settings.NON_FIELD_ERRORS_KEY:
|
||||
exceptions.build_error_from_django_validation_error(exc)
|
||||
}
|
||||
elif isinstance(exc.detail, dict):
|
||||
# If errors may be a dict we use the standard {key: list of values}.
|
||||
|
@ -422,8 +424,9 @@ class Serializer(BaseSerializer):
|
|||
message = self.error_messages['invalid'].format(
|
||||
datatype=type(data).__name__
|
||||
)
|
||||
error = ValidationError(message, code='invalid')
|
||||
raise ValidationError({
|
||||
api_settings.NON_FIELD_ERRORS_KEY: [message]
|
||||
api_settings.NON_FIELD_ERRORS_KEY: [error]
|
||||
})
|
||||
|
||||
ret = OrderedDict()
|
||||
|
@ -438,9 +441,11 @@ class Serializer(BaseSerializer):
|
|||
if validate_method is not None:
|
||||
validated_value = validate_method(validated_value)
|
||||
except ValidationError as exc:
|
||||
errors[field.field_name] = exc.detail
|
||||
errors[field.field_name] = exc
|
||||
except DjangoValidationError as exc:
|
||||
errors[field.field_name] = list(exc.messages)
|
||||
errors[field.field_name] = (
|
||||
exceptions.build_error_from_django_validation_error(exc)
|
||||
)
|
||||
except SkipField:
|
||||
pass
|
||||
else:
|
||||
|
@ -575,14 +580,18 @@ class ListSerializer(BaseSerializer):
|
|||
message = self.error_messages['not_a_list'].format(
|
||||
input_type=type(data).__name__
|
||||
)
|
||||
error = ValidationError(
|
||||
message,
|
||||
code='not_a_list'
|
||||
)
|
||||
raise ValidationError({
|
||||
api_settings.NON_FIELD_ERRORS_KEY: [message]
|
||||
api_settings.NON_FIELD_ERRORS_KEY: [error]
|
||||
})
|
||||
|
||||
if not self.allow_empty and len(data) == 0:
|
||||
message = self.error_messages['empty']
|
||||
raise ValidationError({
|
||||
api_settings.NON_FIELD_ERRORS_KEY: [message]
|
||||
api_settings.NON_FIELD_ERRORS_KEY: [ValidationError(message, code='empty_not_allowed')]
|
||||
})
|
||||
|
||||
ret = []
|
||||
|
|
|
@ -60,7 +60,7 @@ class UniqueValidator(object):
|
|||
queryset = self.filter_queryset(value, queryset)
|
||||
queryset = self.exclude_current_instance(queryset)
|
||||
if queryset.exists():
|
||||
raise ValidationError(self.message)
|
||||
raise ValidationError(self.message, code='unique')
|
||||
|
||||
def __repr__(self):
|
||||
return unicode_to_repr('<%s(queryset=%s)>' % (
|
||||
|
@ -101,7 +101,9 @@ class UniqueTogetherValidator(object):
|
|||
return
|
||||
|
||||
missing = {
|
||||
field_name: self.missing_message
|
||||
field_name: ValidationError(
|
||||
self.missing_message,
|
||||
code='required')
|
||||
for field_name in self.fields
|
||||
if field_name not in attrs
|
||||
}
|
||||
|
@ -147,7 +149,8 @@ class UniqueTogetherValidator(object):
|
|||
]
|
||||
if None not in checked_values and queryset.exists():
|
||||
field_names = ', '.join(self.fields)
|
||||
raise ValidationError(self.message.format(field_names=field_names))
|
||||
raise ValidationError(self.message.format(field_names=field_names),
|
||||
code='unique')
|
||||
|
||||
def __repr__(self):
|
||||
return unicode_to_repr('<%s(queryset=%s, fields=%s)>' % (
|
||||
|
@ -185,7 +188,9 @@ class BaseUniqueForValidator(object):
|
|||
'required' state on the fields they are applied to.
|
||||
"""
|
||||
missing = {
|
||||
field_name: self.missing_message
|
||||
field_name: ValidationError(
|
||||
self.missing_message,
|
||||
code='required')
|
||||
for field_name in [self.field, self.date_field]
|
||||
if field_name not in attrs
|
||||
}
|
||||
|
@ -211,7 +216,8 @@ class BaseUniqueForValidator(object):
|
|||
queryset = self.exclude_current_instance(attrs, queryset)
|
||||
if queryset.exists():
|
||||
message = self.message.format(date_field=self.date_field)
|
||||
raise ValidationError({self.field: message})
|
||||
error = ValidationError(message, code='unique')
|
||||
raise ValidationError({self.field: error})
|
||||
|
||||
def __repr__(self):
|
||||
return unicode_to_repr('<%s(queryset=%s, field=%s, date_field=%s)>' % (
|
||||
|
|
|
@ -15,6 +15,7 @@ from django.views.generic import View
|
|||
|
||||
from rest_framework import exceptions, status
|
||||
from rest_framework.compat import set_rollback
|
||||
from rest_framework.exceptions import ValidationError, _force_text_recursive
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.settings import api_settings
|
||||
|
@ -69,7 +70,17 @@ def exception_handler(exc, context):
|
|||
if getattr(exc, 'wait', None):
|
||||
headers['Retry-After'] = '%d' % exc.wait
|
||||
|
||||
if isinstance(exc.detail, (list, dict)):
|
||||
if isinstance(exc.detail, list):
|
||||
data = _force_text_recursive([item.detail if isinstance(item, ValidationError) else item
|
||||
for item in exc.detai])
|
||||
elif isinstance(exc.detail, dict):
|
||||
for field_name, e in exc.detail.items():
|
||||
if hasattr(e, 'detail') and isinstance(e.detail[0], ValidationError):
|
||||
exc.detail[field_name] = e.detail[0].detail
|
||||
elif isinstance(e, ValidationError):
|
||||
exc.detail[field_name] = e.detail
|
||||
else:
|
||||
exc.detail[field_name] = e
|
||||
data = exc.detail
|
||||
else:
|
||||
data = {'detail': exc.detail}
|
||||
|
|
|
@ -39,7 +39,8 @@ class TestSimpleBoundField:
|
|||
serializer.is_valid()
|
||||
|
||||
assert serializer['text'].value == 'x' * 1000
|
||||
assert serializer['text'].errors == ['Ensure this field has no more than 100 characters.']
|
||||
assert serializer['text'].errors.detail[0].detail == ['Ensure this field has no more than 100 characters.']
|
||||
assert serializer['text'].errors.detail[0].code == 'max_length'
|
||||
assert serializer['text'].name == 'text'
|
||||
assert serializer['amount'].value is 123
|
||||
assert serializer['amount'].errors is None
|
||||
|
|
|
@ -10,6 +10,7 @@ from django.utils import timezone
|
|||
|
||||
import rest_framework
|
||||
from rest_framework import serializers
|
||||
from rest_framework.exceptions import ValidationError
|
||||
|
||||
|
||||
# Tests for field keyword arguments and core functionality.
|
||||
|
@ -426,7 +427,13 @@ class FieldValues:
|
|||
for input_value, expected_failure in get_items(self.invalid_inputs):
|
||||
with pytest.raises(serializers.ValidationError) as exc_info:
|
||||
self.field.run_validation(input_value)
|
||||
assert exc_info.value.detail == expected_failure
|
||||
|
||||
if isinstance(exc_info.value.detail[0], ValidationError):
|
||||
failure = exc_info.value.detail[0].detail
|
||||
else:
|
||||
failure = exc_info.value.detail
|
||||
|
||||
assert failure == expected_failure
|
||||
|
||||
def test_outputs(self):
|
||||
for output_value, expected_output in get_items(self.outputs):
|
||||
|
@ -1393,7 +1400,10 @@ class TestFieldFieldWithName(FieldValues):
|
|||
# call into it's regular validation, or require PIL for testing.
|
||||
class FailImageValidation(object):
|
||||
def to_python(self, value):
|
||||
raise serializers.ValidationError(self.error_messages['invalid_image'])
|
||||
raise serializers.ValidationError(
|
||||
self.error_messages['invalid_image'],
|
||||
code='invalid_image'
|
||||
)
|
||||
|
||||
|
||||
class PassImageValidation(object):
|
||||
|
|
|
@ -374,7 +374,7 @@ class TestGenericIPAddressFieldValidation(TestCase):
|
|||
|
||||
s = TestSerializer(data={'address': 'not an ip address'})
|
||||
self.assertFalse(s.is_valid())
|
||||
self.assertEquals(1, len(s.errors['address']),
|
||||
self.assertEquals(1, len(s.errors['address'].detail),
|
||||
'Unexpected number of validation errors: '
|
||||
'{0}'.format(s.errors))
|
||||
|
||||
|
|
|
@ -244,7 +244,8 @@ class HyperlinkedForeignKeyTests(TestCase):
|
|||
instance = ForeignKeySource.objects.get(pk=1)
|
||||
serializer = ForeignKeySourceSerializer(instance, data=data, context={'request': request})
|
||||
self.assertFalse(serializer.is_valid())
|
||||
self.assertEqual(serializer.errors, {'target': ['Incorrect type. Expected URL string, received int.']})
|
||||
self.assertEqual(serializer.errors['target'].detail, ['Incorrect type. Expected URL string, received int.'])
|
||||
self.assertEqual(serializer.errors['target'].code, 'incorrect_type')
|
||||
|
||||
def test_reverse_foreign_key_update(self):
|
||||
data = {'url': 'http://testserver/foreignkeytarget/2/', 'name': 'target-2', 'sources': ['http://testserver/foreignkeysource/1/', 'http://testserver/foreignkeysource/3/']}
|
||||
|
@ -315,7 +316,8 @@ class HyperlinkedForeignKeyTests(TestCase):
|
|||
instance = ForeignKeySource.objects.get(pk=1)
|
||||
serializer = ForeignKeySourceSerializer(instance, data=data, context={'request': request})
|
||||
self.assertFalse(serializer.is_valid())
|
||||
self.assertEqual(serializer.errors, {'target': ['This field may not be null.']})
|
||||
self.assertEqual(serializer.errors['target'].detail, ['This field may not be null.'])
|
||||
self.assertEqual(serializer.errors['target'].code, 'null')
|
||||
|
||||
|
||||
class HyperlinkedNullableForeignKeyTests(TestCase):
|
||||
|
|
|
@ -235,7 +235,9 @@ class PKForeignKeyTests(TestCase):
|
|||
instance = ForeignKeySource.objects.get(pk=1)
|
||||
serializer = ForeignKeySourceSerializer(instance, data=data)
|
||||
self.assertFalse(serializer.is_valid())
|
||||
self.assertEqual(serializer.errors, {'target': ['Incorrect type. Expected pk value, received %s.' % six.text_type.__name__]})
|
||||
self.assertEqual(serializer.errors['target'].detail,
|
||||
['Incorrect type. Expected pk value, received %s.' % six.text_type.__name__])
|
||||
self.assertEqual(serializer.errors['target'].code, 'incorrect_type')
|
||||
|
||||
def test_reverse_foreign_key_update(self):
|
||||
data = {'id': 2, 'name': 'target-2', 'sources': [1, 3]}
|
||||
|
@ -306,7 +308,8 @@ class PKForeignKeyTests(TestCase):
|
|||
instance = ForeignKeySource.objects.get(pk=1)
|
||||
serializer = ForeignKeySourceSerializer(instance, data=data)
|
||||
self.assertFalse(serializer.is_valid())
|
||||
self.assertEqual(serializer.errors, {'target': ['This field may not be null.']})
|
||||
self.assertEqual(serializer.errors['target'].detail, ['This field may not be null.'])
|
||||
self.assertEqual(serializer.errors['target'].code, 'null')
|
||||
|
||||
def test_foreign_key_with_unsaved(self):
|
||||
source = ForeignKeySource(name='source-unsaved')
|
||||
|
|
|
@ -104,7 +104,8 @@ class SlugForeignKeyTests(TestCase):
|
|||
instance = ForeignKeySource.objects.get(pk=1)
|
||||
serializer = ForeignKeySourceSerializer(instance, data=data)
|
||||
self.assertFalse(serializer.is_valid())
|
||||
self.assertEqual(serializer.errors, {'target': ['Object with name=123 does not exist.']})
|
||||
self.assertEqual(serializer.errors['target'].detail, ['Object with name=123 does not exist.'])
|
||||
self.assertEqual(serializer.errors['target'].code, 'does_not_exist')
|
||||
|
||||
def test_reverse_foreign_key_update(self):
|
||||
data = {'id': 2, 'name': 'target-2', 'sources': ['source-1', 'source-3']}
|
||||
|
@ -176,7 +177,8 @@ class SlugForeignKeyTests(TestCase):
|
|||
instance = ForeignKeySource.objects.get(pk=1)
|
||||
serializer = ForeignKeySourceSerializer(instance, data=data)
|
||||
self.assertFalse(serializer.is_valid())
|
||||
self.assertEqual(serializer.errors, {'target': ['This field may not be null.']})
|
||||
self.assertEqual(serializer.errors['target'].detail, ['This field may not be null.'])
|
||||
self.assertEqual(serializer.errors['target'].code, 'null')
|
||||
|
||||
|
||||
class SlugNullableForeignKeyTests(TestCase):
|
||||
|
|
|
@ -7,6 +7,7 @@ import pytest
|
|||
|
||||
from rest_framework import serializers
|
||||
from rest_framework.compat import unicode_repr
|
||||
from rest_framework.fields import DjangoValidationError
|
||||
|
||||
from .utils import MockObject
|
||||
|
||||
|
@ -31,7 +32,8 @@ class TestSerializer:
|
|||
serializer = self.Serializer(data={'char': 'abc'})
|
||||
assert not serializer.is_valid()
|
||||
assert serializer.validated_data == {}
|
||||
assert serializer.errors == {'integer': ['This field is required.']}
|
||||
assert serializer.errors['integer'].detail == ['This field is required.']
|
||||
assert serializer.errors['integer'].code == 'required'
|
||||
|
||||
def test_partial_validation(self):
|
||||
serializer = self.Serializer(data={'char': 'abc'}, partial=True)
|
||||
|
@ -69,7 +71,10 @@ class TestValidateMethod:
|
|||
integer = serializers.IntegerField()
|
||||
|
||||
def validate(self, attrs):
|
||||
raise serializers.ValidationError('Non field error')
|
||||
raise serializers.ValidationError(
|
||||
'Non field error',
|
||||
code='test'
|
||||
)
|
||||
|
||||
serializer = ExampleSerializer(data={'char': 'abc', 'integer': 123})
|
||||
assert not serializer.is_valid()
|
||||
|
@ -309,3 +314,27 @@ class TestCacheSerializerData:
|
|||
pickled = pickle.dumps(serializer.data)
|
||||
data = pickle.loads(pickled)
|
||||
assert data == {'field1': 'a', 'field2': 'b'}
|
||||
|
||||
|
||||
class TestGetValidationErrorDetail:
|
||||
def test_get_validation_error_detail_converts_django_errors(self):
|
||||
exc = DjangoValidationError("Missing field.", code='required')
|
||||
detail = serializers.get_validation_error_detail(exc)
|
||||
assert detail['non_field_errors'][0].detail == ['Missing field.']
|
||||
assert detail['non_field_errors'][0].code == 'required'
|
||||
|
||||
|
||||
class TestCapturingDjangoValidationError:
|
||||
def test_django_validation_error_on_a_field_is_converted(self):
|
||||
class ExampleSerializer(serializers.Serializer):
|
||||
field = serializers.CharField()
|
||||
|
||||
def validate_field(self, value):
|
||||
raise DjangoValidationError(
|
||||
'validation failed'
|
||||
)
|
||||
|
||||
serializer = ExampleSerializer(data={'field': 'a'})
|
||||
assert not serializer.is_valid()
|
||||
assert serializer.errors['field'][0].detail == ['validation failed']
|
||||
assert serializer.errors['field'][0].code == 'invalid'
|
||||
|
|
|
@ -67,15 +67,16 @@ class BulkCreateSerializerTests(TestCase):
|
|||
'author': 'Haruki Murakami'
|
||||
}
|
||||
]
|
||||
expected_errors = [
|
||||
{},
|
||||
{},
|
||||
{'id': ['A valid integer is required.']}
|
||||
]
|
||||
|
||||
serializer = self.BookSerializer(data=data, many=True)
|
||||
self.assertEqual(serializer.is_valid(), False)
|
||||
self.assertEqual(serializer.errors, expected_errors)
|
||||
|
||||
for idx, error in enumerate(serializer.errors):
|
||||
if idx < 2:
|
||||
self.assertEqual(error, {})
|
||||
else:
|
||||
self.assertEqual(error['id'].detail, ['A valid integer is required.'])
|
||||
self.assertEqual(error['id'].code, 'invalid')
|
||||
|
||||
def test_invalid_list_datatype(self):
|
||||
"""
|
||||
|
@ -87,13 +88,10 @@ class BulkCreateSerializerTests(TestCase):
|
|||
|
||||
text_type_string = six.text_type.__name__
|
||||
message = 'Invalid data. Expected a dictionary, but got %s.' % text_type_string
|
||||
expected_errors = [
|
||||
{'non_field_errors': [message]},
|
||||
{'non_field_errors': [message]},
|
||||
{'non_field_errors': [message]}
|
||||
]
|
||||
|
||||
self.assertEqual(serializer.errors, expected_errors)
|
||||
for error in serializer.errors:
|
||||
self.assertEqual(error['non_field_errors'][0].detail, [message])
|
||||
self.assertEqual(error['non_field_errors'][0].code, 'invalid')
|
||||
|
||||
def test_invalid_single_datatype(self):
|
||||
"""
|
||||
|
@ -103,9 +101,9 @@ class BulkCreateSerializerTests(TestCase):
|
|||
serializer = self.BookSerializer(data=data, many=True)
|
||||
self.assertEqual(serializer.is_valid(), False)
|
||||
|
||||
expected_errors = {'non_field_errors': ['Expected a list of items but got type "int".']}
|
||||
|
||||
self.assertEqual(serializer.errors, expected_errors)
|
||||
self.assertEqual(serializer.errors['non_field_errors'][0].detail,
|
||||
['Expected a list of items but got type "int".'])
|
||||
self.assertEqual(serializer.errors['non_field_errors'][0].code, 'not_a_list')
|
||||
|
||||
def test_invalid_single_object(self):
|
||||
"""
|
||||
|
@ -120,6 +118,7 @@ class BulkCreateSerializerTests(TestCase):
|
|||
serializer = self.BookSerializer(data=data, many=True)
|
||||
self.assertEqual(serializer.is_valid(), False)
|
||||
|
||||
expected_errors = {'non_field_errors': ['Expected a list of items but got type "dict".']}
|
||||
self.assertEqual(serializer.errors['non_field_errors'][0].detail,
|
||||
['Expected a list of items but got type "dict".'])
|
||||
|
||||
self.assertEqual(serializer.errors, expected_errors)
|
||||
self.assertEqual(serializer.errors['non_field_errors'][0].code, 'not_a_list')
|
||||
|
|
|
@ -280,7 +280,10 @@ class TestListSerializerClass:
|
|||
def test_list_serializer_class_validate(self):
|
||||
class CustomListSerializer(serializers.ListSerializer):
|
||||
def validate(self, attrs):
|
||||
raise serializers.ValidationError('Non field error')
|
||||
raise serializers.ValidationError(
|
||||
'Non field error',
|
||||
code='test'
|
||||
)
|
||||
|
||||
class TestSerializer(serializers.Serializer):
|
||||
class Meta:
|
||||
|
|
|
@ -113,8 +113,8 @@ class TestNestedSerializerWithMany:
|
|||
|
||||
assert not serializer.is_valid()
|
||||
|
||||
expected_errors = {'not_allow_null': [serializer.error_messages['null']]}
|
||||
assert serializer.errors == expected_errors
|
||||
assert serializer.errors['not_allow_null'].detail == [serializer.error_messages['null']]
|
||||
assert serializer.errors['not_allow_null'].code == 'null'
|
||||
|
||||
def test_run_the_field_validation_even_if_the_field_is_null(self):
|
||||
class TestSerializer(self.Serializer):
|
||||
|
@ -165,5 +165,7 @@ class TestNestedSerializerWithMany:
|
|||
|
||||
assert not serializer.is_valid()
|
||||
|
||||
expected_errors = {'not_allow_empty': {'non_field_errors': [serializers.ListSerializer.default_error_messages['empty']]}}
|
||||
assert serializer.errors == expected_errors
|
||||
assert serializer.errors['not_allow_empty'].detail['non_field_errors'][0].detail == \
|
||||
[serializers.ListSerializer.default_error_messages['empty']]
|
||||
|
||||
assert serializer.errors['not_allow_empty'].detail['non_field_errors'][0].code == 'empty_not_allowed'
|
||||
|
|
|
@ -41,7 +41,8 @@ class ShouldValidateModelSerializer(serializers.ModelSerializer):
|
|||
|
||||
def validate_renamed(self, value):
|
||||
if len(value) < 3:
|
||||
raise serializers.ValidationError('Minimum 3 characters.')
|
||||
raise serializers.ValidationError('Minimum 3 characters.',
|
||||
code='min_length')
|
||||
return value
|
||||
|
||||
class Meta:
|
||||
|
@ -91,11 +92,9 @@ class TestAvoidValidation(TestCase):
|
|||
def test_serializer_errors_has_only_invalid_data_error(self):
|
||||
serializer = ValidationSerializer(data='invalid data')
|
||||
self.assertFalse(serializer.is_valid())
|
||||
self.assertDictEqual(serializer.errors, {
|
||||
'non_field_errors': [
|
||||
'Invalid data. Expected a dictionary, but got %s.' % type('').__name__
|
||||
]
|
||||
})
|
||||
self.assertEqual(serializer.errors['non_field_errors'][0].detail,
|
||||
['Invalid data. Expected a dictionary, but got %s.' % type('').__name__])
|
||||
self.assertEqual(serializer.errors['non_field_errors'][0].code, 'invalid')
|
||||
|
||||
|
||||
# regression tests for issue: 1493
|
||||
|
@ -123,7 +122,9 @@ class TestMaxValueValidatorValidation(TestCase):
|
|||
def test_max_value_validation_serializer_fails(self):
|
||||
serializer = ValidationMaxValueValidatorModelSerializer(data={'number_value': 101})
|
||||
self.assertFalse(serializer.is_valid())
|
||||
self.assertDictEqual({'number_value': ['Ensure this value is less than or equal to 100.']}, serializer.errors)
|
||||
self.assertEqual(['Ensure this value is less than or equal to 100.'], serializer.errors['number_value'].detail[0].detail)
|
||||
self.assertEqual(None, serializer.errors['number_value'].code)
|
||||
self.assertEqual('max_value', serializer.errors['number_value'].detail[0].code)
|
||||
|
||||
def test_max_value_validation_success(self):
|
||||
obj = ValidationMaxValueValidatorModel.objects.create(number_value=100)
|
||||
|
|
95
tests/test_validation_error.py
Normal file
95
tests/test_validation_error.py
Normal file
|
@ -0,0 +1,95 @@
|
|||
import pytest
|
||||
from django.test import TestCase
|
||||
|
||||
from rest_framework import serializers, status
|
||||
from rest_framework.decorators import api_view
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.settings import api_settings
|
||||
from rest_framework.test import APIRequestFactory
|
||||
from rest_framework.views import APIView
|
||||
|
||||
factory = APIRequestFactory()
|
||||
|
||||
|
||||
class ExampleSerializer(serializers.Serializer):
|
||||
char = serializers.CharField()
|
||||
integer = serializers.IntegerField()
|
||||
|
||||
|
||||
class ErrorView(APIView):
|
||||
def get(self, request, *args, **kwargs):
|
||||
ExampleSerializer(data={}).is_valid(raise_exception=True)
|
||||
|
||||
|
||||
@api_view(['GET'])
|
||||
def error_view(request):
|
||||
ExampleSerializer(data={}).is_valid(raise_exception=True)
|
||||
|
||||
|
||||
class TestValidationErrorWithCode(TestCase):
|
||||
def setUp(self):
|
||||
self.DEFAULT_HANDLER = api_settings.EXCEPTION_HANDLER
|
||||
|
||||
def exception_handler(exc, request):
|
||||
if not exc.code:
|
||||
errors = {
|
||||
field_name: {
|
||||
'code': e.code,
|
||||
'message': e.detail
|
||||
} for field_name, e in exc.detail.items()
|
||||
}
|
||||
else:
|
||||
errors = {
|
||||
'code': exc.code,
|
||||
'detail': exc.detail
|
||||
}
|
||||
return Response(errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
api_settings.EXCEPTION_HANDLER = exception_handler
|
||||
|
||||
self.expected_response_data = {
|
||||
'char': {
|
||||
'message': ['This field is required.'],
|
||||
'code': 'required',
|
||||
},
|
||||
'integer': {
|
||||
'message': ['This field is required.'],
|
||||
'code': 'required'
|
||||
},
|
||||
}
|
||||
|
||||
def tearDown(self):
|
||||
api_settings.EXCEPTION_HANDLER = self.DEFAULT_HANDLER
|
||||
|
||||
def test_validation_error_requires_no_code_for_structured_errors(self):
|
||||
"""
|
||||
ValidationError can hold a list or dictionary of simple errors, in
|
||||
which case the code is no longer meaningful and should not be
|
||||
specified.
|
||||
"""
|
||||
with pytest.raises(AssertionError):
|
||||
serializers.ValidationError([ValidationError("test-detail", "test-code")], code='min_value')
|
||||
|
||||
with pytest.raises(AssertionError):
|
||||
serializers.ValidationError({}, code='min_value')
|
||||
|
||||
def test_validation_error_stores_error_code(self):
|
||||
exception = serializers.ValidationError("", code='min_value')
|
||||
assert exception.code == 'min_value'
|
||||
|
||||
def test_class_based_view_exception_handler(self):
|
||||
view = ErrorView.as_view()
|
||||
|
||||
request = factory.get('/', content_type='application/json')
|
||||
response = view(request)
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertEqual(response.data, self.expected_response_data)
|
||||
|
||||
def test_function_based_view_exception_handler(self):
|
||||
view = error_view
|
||||
|
||||
request = factory.get('/', content_type='application/json')
|
||||
response = view(request)
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertEqual(response.data, self.expected_response_data)
|
|
@ -47,8 +47,11 @@ class TestUniquenessValidation(TestCase):
|
|||
def test_is_not_unique(self):
|
||||
data = {'username': 'existing'}
|
||||
serializer = UniquenessSerializer(data=data)
|
||||
|
||||
assert not serializer.is_valid()
|
||||
assert serializer.errors == {'username': ['UniquenessModel with this username already exists.']}
|
||||
assert serializer.errors['username'].code is None
|
||||
assert serializer.errors['username'].detail[0].code == 'unique'
|
||||
assert serializer.errors['username'].detail[0].detail == ['UniquenessModel with this username already exists.']
|
||||
|
||||
def test_is_unique(self):
|
||||
data = {'username': 'other'}
|
||||
|
@ -150,11 +153,9 @@ class TestUniquenessTogetherValidation(TestCase):
|
|||
data = {'race_name': 'example', 'position': 2}
|
||||
serializer = UniquenessTogetherSerializer(data=data)
|
||||
assert not serializer.is_valid()
|
||||
assert serializer.errors == {
|
||||
'non_field_errors': [
|
||||
'The fields race_name, position must make a unique set.'
|
||||
]
|
||||
}
|
||||
assert serializer.errors['non_field_errors'][0].code == 'unique'
|
||||
assert serializer.errors['non_field_errors'][0].detail == [
|
||||
'The fields race_name, position must make a unique set.']
|
||||
|
||||
def test_is_unique_together(self):
|
||||
"""
|
||||
|
@ -189,9 +190,8 @@ class TestUniquenessTogetherValidation(TestCase):
|
|||
data = {'position': 2}
|
||||
serializer = UniquenessTogetherSerializer(data=data, partial=True)
|
||||
assert not serializer.is_valid()
|
||||
assert serializer.errors == {
|
||||
'race_name': ['This field is required.']
|
||||
}
|
||||
assert serializer.errors['race_name'][0].code == 'required'
|
||||
assert serializer.errors['race_name'][0].detail == ['This field is required.']
|
||||
|
||||
def test_ignore_excluded_fields(self):
|
||||
"""
|
||||
|
@ -278,9 +278,8 @@ class TestUniquenessForDateValidation(TestCase):
|
|||
data = {'slug': 'existing', 'published': '2000-01-01'}
|
||||
serializer = UniqueForDateSerializer(data=data)
|
||||
assert not serializer.is_valid()
|
||||
assert serializer.errors == {
|
||||
'slug': ['This field must be unique for the "published" date.']
|
||||
}
|
||||
assert serializer.errors['slug'][0].code == 'unique'
|
||||
assert serializer.errors['slug'][0].detail == ['This field must be unique for the "published" date.']
|
||||
|
||||
def test_is_unique_for_date(self):
|
||||
"""
|
||||
|
|
Loading…
Reference in New Issue
Block a user