from rest_framework import serializers from rest_framework import exceptions as rest_exceptions from rest_framework.views import exception_handler from rest_framework.settings import api_settings from rest_framework import exceptions from django.core.exceptions import ValidationError def create_serializer_class(name, fields): return type(name, (serializers.Serializer, ), fields) def inline_serializer(*, fields, data=None, **kwargs): serializer_class = create_serializer_class(name='', fields=fields) if data is not None: return serializer_class(data=data, **kwargs) return serializer_class(**kwargs) def get_first_matching_attr(obj, *attrs, default=None): for attr in attrs: if hasattr(obj, attr): return getattr(obj, attr) return default def get_error_message(exc): if hasattr(exc, 'message_dict'): return exc.message_dict error_msg = get_first_matching_attr(exc, 'message', 'messages') if isinstance(error_msg, list): error_msg = ', '.join(error_msg) if error_msg is None: error_msg = str(exc) return error_msg class ApiErrorsMixin: """ Mixin that transforms Django and Python exceptions into rest_framework ones. without the mixin, they return 500 status code which is not desired. """ expected_exceptions = { ValueError: rest_exceptions.ValidationError, ValidationError: rest_exceptions.ValidationError, PermissionError: rest_exceptions.PermissionDenied } def handle_exception(self, exc): if isinstance(exc, tuple(self.expected_exceptions.keys())): drf_exception_class = self.expected_exceptions[exc.__class__] drf_exception = drf_exception_class(get_error_message(exc)) return super().handle_exception(drf_exception) return super().handle_exception(exc) class ErrorsFormatter: """ The current formatter gets invalid serializer errors, uses DRF standart for code and messaging and then parses it to the following format: { "errors": [ { "message": "Error message", "code": "Some code", "field": "field_name" }, { "message": "Error message", "code": "Some code", "field": "nested.field_name" }, ... ] } """ FIELD = 'field' MESSAGE = 'message' CODE = 'code' ERRORS = 'errors' def __init__(self, exception): self.exception = exception def __call__(self): if hasattr(self.exception, 'get_full_details'): formatted_errors = self._get_response_json_from_drf_errors( serializer_errors=self.exception.get_full_details() ) else: formatted_errors = self._get_response_json_from_error_message(message=str(self.exception)) return formatted_errors def _get_response_json_from_drf_errors(self, serializer_errors=None): if serializer_errors is None: serializer_errors = {} if type(serializer_errors) is list: serializer_errors = { api_settings.NON_FIELD_ERRORS_KEY: serializer_errors } list_of_errors = self._get_list_of_errors(errors_dict=serializer_errors) response_data = { self.ERRORS: list_of_errors } return response_data def _get_response_json_from_error_message(self, *, message='', field=None, code='error'): response_data = { self.ERRORS: [ { self.MESSAGE: message, self.CODE: code } ] } if field: response_data[self.ERRORS][self.FIELD] = field return response_data def _unpack(self, obj): if type(obj) is list and len(obj) == 1: return obj[0] return obj def _get_list_of_errors(self, field_path='', errors_dict=None): """ Error_dict is in the following format: { 'field1': { 'message': 'some message..' 'code' 'some code...' }, 'field2: ...' } """ if errors_dict is None: return [] message_value = errors_dict.get(self.MESSAGE, None) # Note: If 'message' is name of a field we don't want to stop the recursion here! if message_value is not None and\ (type(message_value) in {str, exceptions.ErrorDetail}): if field_path: errors_dict[self.FIELD] = field_path return [errors_dict] errors_list = [] for key, value in errors_dict.items(): new_field_path = '{0}.{1}'.format(field_path, key) if field_path else key key_is_non_field_errors = key == api_settings.NON_FIELD_ERRORS_KEY if type(value) is list: current_level_error_list = [] new_value = value for error in new_value: # if the type of field_error is list we need to unpack it field_error = self._unpack(error) if not key_is_non_field_errors: field_error[self.FIELD] = new_field_path current_level_error_list.append(field_error) else: path = field_path if key_is_non_field_errors else new_field_path current_level_error_list = self._get_list_of_errors(field_path=path, errors_dict=value) errors_list += current_level_error_list return errors_list def exception_errors_format_handler(exc, context): response = exception_handler(exc, context) # If unexpected error occurs (server error, etc.) if response is None: return response formatter = ErrorsFormatter(exc) response.data = formatter() return response