diff --git a/rest_framework/exceptions.py b/rest_framework/exceptions.py index 7cee4468c..2d1474050 100644 --- a/rest_framework/exceptions.py +++ b/rest_framework/exceptions.py @@ -58,22 +58,74 @@ class APIException(Exception): return self.detail +def build_error_from_django_validation_error(exc_info): + code = getattr(exc_info, 'code', None) or 'invalid' + return [ + (msg, 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: # # from rest_framework import serializers # raise serializers.ValidationError('Value was invalid') + + class ValidationError(APIException): status_code = status.HTTP_400_BAD_REQUEST + code = None 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) - self.code = code + + if code: + self.full_details = [(detail, code)] + else: + self.full_details = detail + + if isinstance(self.full_details, tuple): + self.detail, self.code = self.full_details + self.detail = [self.detail] + + elif isinstance(self.full_details, list): + if isinstance(self.full_details, ReturnList): + self.detail = ReturnList(serializer=self.full_details.serializer) + else: + self.detail = [] + for error in self.full_details: + if isinstance(error, tuple): + message, code = error + self.detail.append(message) + elif isinstance(error, dict): + self.detail = self.full_details + break + + elif isinstance(self.full_details, dict): + if isinstance(self.full_details, ReturnDict): + self.detail = ReturnDict(serializer=self.full_details.serializer) + else: + self.detail = {} + + for field_name, errors in self.full_details.items(): + self.detail[field_name] = [] + if isinstance(errors, tuple): + message, code = errors + self.detail[field_name].append(message) + elif isinstance(errors, list): + for error in errors: + if isinstance(error, tuple): + message, code = error + else: + message = error + if message: + self.detail[field_name].append(message) + else: + self.detail = [self.full_details] + + self.detail = _force_text_recursive(self.detail) def __str__(self): return six.text_type(self.detail) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 27e064040..35df9225c 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -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,9 @@ class Field(object): # attempting to accumulate a list of errors. if isinstance(exc.detail, dict): raise - errors.extend(exc.detail) + errors.append((exc.detail, exc.code)) except DjangoValidationError as exc: - errors.extend(exc.messages) + errors.extend(build_error_from_django_validation_error(exc)) if errors: raise ValidationError(errors) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 99d36a8a5..27e764da3 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -23,6 +23,7 @@ from django.utils.translation import ugettext_lazy as _ 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 +from rest_framework.exceptions import build_error_from_django_validation_error from rest_framework.utils import model_meta from rest_framework.utils.field_mapping import ( ClassLookupDict, get_field_kwargs, get_nested_relation_kwargs, @@ -213,12 +214,12 @@ class BaseSerializer(Field): self._validated_data = self.run_validation(self.initial_data) except ValidationError as exc: self._validated_data = {} - self._errors = exc.detail + self._errors = exc.full_details else: self._errors = {} if self._errors and raise_exception: - raise ValidationError(self.errors) + raise ValidationError(self._errors) return not bool(self._errors) @@ -248,7 +249,36 @@ class BaseSerializer(Field): if not hasattr(self, '_errors'): msg = 'You must call `.is_valid()` before accessing `.errors`.' raise AssertionError(msg) - return self._errors + + if isinstance(self._errors, dict): + errors = ReturnDict(serializer=self) + for key, value in self._errors.items(): + if isinstance(value, dict): + errors[key] = {} + for key_, value_ in value.items(): + message, code = value_[0] + errors[key][key_] = [message] + elif isinstance(value, list): + if isinstance(value[0], tuple): + message, code = value[0] + else: + message = value[0] + if isinstance(message, list): + errors[key] = message + else: + errors[key] = [message] + elif isinstance(value, tuple): + message, code = value + errors[key] = [message] + else: + errors[key] = [value] + elif isinstance(self._errors, list): + errors = ReturnList(self._errors, serializer=self) + else: + # This shouldn't ever happen. + errors = self._errors + + return errors @property def validated_data(self): @@ -299,24 +329,25 @@ def get_validation_error_detail(exc): # inside your codebase, but we handle Django's validation # exception class as well for simpler compat. # Eg. Calling Model.clean() explicitly inside Serializer.validate() + error = build_error_from_django_validation_error(exc) return { - api_settings.NON_FIELD_ERRORS_KEY: list(exc.messages) + api_settings.NON_FIELD_ERRORS_KEY: error } - elif isinstance(exc.detail, dict): + elif isinstance(exc.full_details, dict): # If errors may be a dict we use the standard {key: list of values}. # Here we ensure that all the values are *lists* of errors. return { key: value if isinstance(value, list) else [value] - for key, value in exc.detail.items() + for key, value in exc.full_details.items() } - elif isinstance(exc.detail, list): + elif isinstance(exc.full_details, list): # Errors raised as a list are non-field errors. return { - api_settings.NON_FIELD_ERRORS_KEY: exc.detail + api_settings.NON_FIELD_ERRORS_KEY: exc.full_details } # Errors raised as a string are non-field errors. return { - api_settings.NON_FIELD_ERRORS_KEY: [exc.detail] + api_settings.NON_FIELD_ERRORS_KEY: [exc.full_details] } @@ -422,12 +453,13 @@ class Serializer(BaseSerializer): message = self.error_messages['invalid'].format( datatype=type(data).__name__ ) + code = 'invalid' raise ValidationError({ - api_settings.NON_FIELD_ERRORS_KEY: [message] + api_settings.NON_FIELD_ERRORS_KEY: [(message, code)] }) - ret = OrderedDict() - errors = OrderedDict() + ret = ReturnDict(serializer=self) + errors = ReturnDict(serializer=self) fields = self._writable_fields for field in fields: @@ -438,9 +470,10 @@ 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.full_details except DjangoValidationError as exc: - errors[field.field_name] = list(exc.messages) + error = build_error_from_django_validation_error(exc) + errors[field.field_name] = error except SkipField: pass else: @@ -575,14 +608,16 @@ class ListSerializer(BaseSerializer): message = self.error_messages['not_a_list'].format( input_type=type(data).__name__ ) + code = 'not_a_list' raise ValidationError({ - api_settings.NON_FIELD_ERRORS_KEY: [message] + api_settings.NON_FIELD_ERRORS_KEY: [(message, code)] }) if not self.allow_empty and len(data) == 0: message = self.error_messages['empty'] + code = 'empty_not_allowed' raise ValidationError({ - api_settings.NON_FIELD_ERRORS_KEY: [message] + api_settings.NON_FIELD_ERRORS_KEY: [(message, code)] }) ret = [] diff --git a/rest_framework/validators.py b/rest_framework/validators.py index d96421542..8fe629c72 100644 --- a/rest_framework/validators.py +++ b/rest_framework/validators.py @@ -100,8 +100,9 @@ class UniqueTogetherValidator(object): if self.instance is not None: return + code = 'required' missing = { - field_name: self.missing_message + field_name: [(self.missing_message, code)] for field_name in self.fields if field_name not in attrs } @@ -186,8 +187,9 @@ class BaseUniqueForValidator(object): The `UniqueForValidator` classes always force an implied 'required' state on the fields they are applied to. """ + code = 'required' missing = { - field_name: self.missing_message + field_name: [(self.missing_message, code)] for field_name in [self.field, self.date_field] if field_name not in attrs } @@ -213,9 +215,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, - }) + code = 'unique' + raise ValidationError({self.field: [(message, code)]}) def __repr__(self): return unicode_to_repr('<%s(queryset=%s, field=%s, date_field=%s)>' % ( diff --git a/tests/test_validation_error.py b/tests/test_validation_error.py index a9d244176..fbde19022 100644 --- a/tests/test_validation_error.py +++ b/tests/test_validation_error.py @@ -31,12 +31,12 @@ class TestValidationErrorWithCode(TestCase): def exception_handler(exc, request): return_errors = {} - for field_name, errors in exc.detail.items(): + for field_name, errors in exc.full_details.items(): return_errors[field_name] = [] - for error in errors: + for message, code in errors: return_errors[field_name].append({ - 'code': error.code, - 'message': error + 'code': code, + 'message': message }) return Response(return_errors, status=status.HTTP_400_BAD_REQUEST)