-
-
Save twidi/9d55486c36b6a51bdcb05ce3a763e79f to your computer and use it in GitHub Desktop.
""" | |
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) |
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)
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
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
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
Where could I be going wrong?
Hard to say without seeing your adaption.
Very helpful snippet. Thanks!
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)
Please what is the context and how do I pass that to the django view?
awesome! thanks