Skip to content

Instantly share code, notes, and snippets.

@twidi
Created December 21, 2016 14:34
Show Gist options
  • Star 45 You must be signed in to star a gist
  • Fork 7 You must be signed in to fork a gist
  • Save twidi/9d55486c36b6a51bdcb05ce3a763e79f to your computer and use it in GitHub Desktop.
Save twidi/9d55486c36b6a51bdcb05ce3a763e79f to your computer and use it in GitHub Desktop.
Make Django Rest Framework correctly handle Django ValidationError raised in the save method of a model
"""
Sometimes in your Django model you want to raise a ``ValidationError`` in the ``save`` method, for
some reason.
This exception is not managed by Django Rest Framework because it occurs after its validation
process. So at the end, you'll have a 500.
Correcting this is as simple as overriding the exception handler, by converting the Django
``ValidationError`` to a DRF one.
"""
from django.core.exceptions import ValidationError as DjangoValidationError
from rest_framework.exceptions import ValidationError as DRFValidationError
from rest_framework.views import exception_handler as drf_exception_handler
def exception_handler(exc, context):
"""Handle Django ValidationError as an accepted exception
Must be set in settings:
>>> REST_FRAMEWORK = {
... # ...
... 'EXCEPTION_HANDLER': 'mtp.apps.common.drf.exception_handler',
... # ...
... }
For the parameters, see ``exception_handler``
"""
if isinstance(exc, DjangoValidationError):
exc = DRFValidationError(detail=exc.message_dict)
return drf_exception_handler(exc, context)
@hnykda
Copy link

hnykda commented Oct 31, 2017

awesome! thanks

@adriaanwm
Copy link

adriaanwm commented Nov 10, 2017

Nice! This saved me a lot of trouble, thank you. I don't know why this isn't the default. Most solutions online seem to recommend putting validations in the serializer which is terrible.

I was getting an exception when raising ValidationErrors like ValidationError('error detail') so I changed it to

    if isinstance(exc, DjangoValidationError):
        if hasattr(exc, 'message_dict'):
            exc = DRFValidationError(detail=exc.message_dict)
        else:
            exc = DRFValidationError(detail=exc.message)

    return drf_exception_handler(exc, context)

@smarden1
Copy link

smarden1 commented Nov 22, 2017

I added some more detail to my implementation...

def transform_exceptions(exception):
    if isinstance(exception, DjangoValidationError):
        if hasattr(exception, 'message_dict'):
            detail = exception.message_dict
        elif hasattr(exception, 'message'):
            detail = exception.message
        elif hasattr(exception, 'messages'):
            detail = exception.messages
        else:
            logging.error("BAD VALIDATION MESSAGE: %s" % exception)

        exception = RestValidationError(detail=detail)

    return exception

@jesselang
Copy link

After reading up on Understanding Django REST Framework and Model.full_clean() (updated link), I see what DRF developers are getting at. I took smarden1's implementation and made a minor adjustment to it. But in the end, after reading Christie's article on Django models, encapsulation and data integrity, I ended up refactoring a bit such that this implementation wasn't necessary.

I'm leaving it here in case someone (including a future version of me) can use it.

import logging

from django.core.exceptions import ValidationError as DjangoValidationError
from rest_framework.exceptions import ValidationError

LOG = logging.getLogger(__name__)


def transform_exception(exception):
    """Transform model validation errors into an equivalent DRF ValidationError.
    After reading the references, you may decide not to use this.

    References:
    https://www.kye.id.au/blog/understanding-django-rest-framework-model-full-clean/
    https://www.dabapps.com/blog/django-models-and-encapsulation/
    """
    if isinstance(exception, DjangoValidationError):
        if hasattr(exception, "message_dict"):
            detail = exception.message_dict
        elif hasattr(exception, "message"):
            detail = exception.message
        elif hasattr(exception, "messages"):
            detail = exception.messages
        else:
            LOG.error("BAD VALIDATION MESSAGE: %s", exception)

        exception = ValidationError(detail=detail)

    return exception

@pmburu
Copy link

pmburu commented May 3, 2020

Hi @jesselang,

Thank you for this wonderful piece of code. I adapted it for my project which gets a ValidationError from Django Model save().

But I am getting the following error: TypeError: 'list' object is not callable.

Please see error log

System check identified no issues (0 silenced).
May 03, 2020 - 02:59:29
Django version 3.0.5, using settings 'WashAPI.settings'
Starting development server at http://127.0.0.1:5000/
Quit the server with CONTROL-C.
Internal Server Error: /api/v1/bookings/booking/
Traceback (most recent call last):
  File "/Users/petermburu/Dev/WashMeApp/venv/lib/python3.8/site-packages/rest_framework/views.py", line 502, in dispatch
    response = handler(request, *args, **kwargs)
  File "/Users/petermburu/Dev/WashMeApp/venv/lib/python3.8/site-packages/rest_framework/mixins.py", line 19, in create
    self.perform_create(serializer)
  File "/Users/petermburu/Dev/WashMeApp/venv/lib/python3.8/site-packages/rest_framework/mixins.py", line 24, in perform_create
    serializer.save()
  File "/Users/petermburu/Dev/WashMeApp/venv/lib/python3.8/site-packages/rest_framework/serializers.py", line 212, in save
    self.instance = self.create(validated_data)
  File "/Users/petermburu/Dev/WashMeApp/WashAPI/wash_bookings/serializers.py", line 150, in create
    return Booking.objects.create(driver=drv, **validated_data)
  File "/Users/petermburu/Dev/WashMeApp/venv/lib/python3.8/site-packages/django/db/models/manager.py", line 82, in manager_method
    return getattr(self.get_queryset(), name)(*args, **kwargs)
  File "/Users/petermburu/Dev/WashMeApp/venv/lib/python3.8/site-packages/django/db/models/query.py", line 433, in create
    obj.save(force_insert=True, using=self.db)
  File "/Users/petermburu/Dev/WashMeApp/WashAPI/wash_bookings/models.py", line 165, in save
    self.clean()
  File "/Users/petermburu/Dev/WashMeApp/WashAPI/wash_bookings/models.py", line 156, in clean
    raise ValidationError(
django.core.exceptions.ValidationError: ['All our drivers are booked at: 2020-05-28, 15:00:00-16:00:00']

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/Users/petermburu/Dev/WashMeApp/venv/lib/python3.8/site-packages/django/core/handlers/exception.py", line 34, in inner
    response = get_response(request)
  File "/Users/petermburu/Dev/WashMeApp/venv/lib/python3.8/site-packages/django/core/handlers/base.py", line 115, in _get_response
    response = self.process_exception_by_middleware(e, request)
  File "/Users/petermburu/Dev/WashMeApp/venv/lib/python3.8/site-packages/django/core/handlers/base.py", line 113, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
  File "/Users/petermburu/Dev/WashMeApp/venv/lib/python3.8/site-packages/django/views/decorators/csrf.py", line 54, in wrapped_view
    return view_func(*args, **kwargs)
  File "/Users/petermburu/Dev/WashMeApp/venv/lib/python3.8/site-packages/rest_framework/viewsets.py", line 114, in view
    return self.dispatch(request, *args, **kwargs)
  File "/Users/petermburu/Dev/WashMeApp/WashAPI/wash_bookings/views.py", line 80, in dispatch
    response = super().dispatch(*args, **kwargs)
  File "/Users/petermburu/Dev/WashMeApp/venv/lib/python3.8/site-packages/rest_framework/views.py", line 505, in dispatch
    response = self.handle_exception(exc)
  File "/Users/petermburu/Dev/WashMeApp/venv/lib/python3.8/site-packages/rest_framework/views.py", line 462, in handle_exception
    response = exception_handler(exc, context)
TypeError: 'list' object is not callable

@pmburu
Copy link

pmburu commented May 3, 2020

Where could I be going wrong?

@jesselang
Copy link

Hard to say without seeing your adaption.

@mtolkacz
Copy link

Very helpful snippet. Thanks!

@rabbit-aaron
Copy link

If you think about it... this must have been done already.
As DRF has this ModelSerializer, which magically handles Django's validation just fine (like the ones raised by Model.clean method as well as the validators you specified for your fields)

Here's where the magic happens:

https://github.com/encode/django-rest-framework/blob/master/rest_framework/fields.py
rest_framework.fields.get_error_detail

and here is where it's used, very nicely implemented.

rest_framework.fields.Field.run_validators
https://github.com/encode/django-rest-framework/blob/master/rest_framework/fields.py

and rest_framework.serializers.Serializer.as_serializer_error
https://github.com/encode/django-rest-framework/blob/master/rest_framework/serializers.py

You should be able to handle it like this:

from django.core.exceptions import ValidationError as DjangoValidationError
from rest_framework.exceptions import ValidationError as DRFValidationError
from rest_framework.views import exception_handler as drf_exception_handler
from rest_framework.fields import get_error_detail


def exception_handler(exc, context):
    if isinstance(exc, DjangoValidationError):
        exc = DRFValidationError(detail=get_error_detail(exc))

    return drf_exception_handler(exc, context)

@iamunadike
Copy link

Please what is the context and how do I pass that to the django view?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment