Django REST Framework how to specify error code when raising validation error in serializer
Asked Answered
P

4

38

I have an API endpoint that allow users to register an account. I would like to return HTTP 409 instead of 400 for a duplicate username.

Here is my serializer:

from django.contrib.auth.models import User
from rest_framework.serializers import ModelSerializer

class UserSerializer(ModelSerializer):
    username = CharField()

    def validate_username(self, value):
        if User.objects.filter(username=value).exists():
            raise NameDuplicationError()
        return value


class NameDuplicationError(APIException):
    status_code = status.HTTP_409_CONFLICT
    default_detail = u'Duplicate Username'

When the error is triggered, the response is: {"detail":"Duplicate Username"}. I realised that if I subclass APIException, the key detail is used instead of username.

I want to have this response instead {"username":"Duplicate Username"}

or I would like to specify a status code when raising a ValidationError:

def validate_username(self, value):
    if User.objects.filter(username=value).exists():
        raise serializers.ValidationError('Duplicate Username', 
                                          status_code=status.HTTP_409_CONFLICT)
    return value

But this does not work as ValidationError only returns 400.

Is there any other way to accomplish this?

Pameliapamelina answered 2/11, 2015 at 10:19 Comment(0)
S
44

You can raise different exceptions like:

from rest_framework.exceptions import APIException
from django.utils.encoding import force_text
from rest_framework import status


class CustomValidation(APIException):
    status_code = status.HTTP_500_INTERNAL_SERVER_ERROR
    default_detail = 'A server error occurred.'

    def __init__(self, detail, field, status_code):
        if status_code is not None:self.status_code = status_code
        if detail is not None:
            self.detail = {field: force_text(detail)}
        else: self.detail = {'detail': force_text(self.default_detail)}

you can use this in your serializer like:

raise CustomValidation('Duplicate Username','username', status_code=status.HTTP_409_CONFLICT)

or

raise CustomValidation('Access denied','username', status_code=status.HTTP_403_FORBIDDEN)
Swart answered 2/11, 2015 at 11:57 Comment(2)
ternary operator may be cleaner here, self.status_code = status_code if status_code else status.HTTP_500_INTERNAL_SERVER_ERRORPaxton
This is good, but as-is default values aren't provided for detail, field and status_code making them required on instantiation. (is not None check won't happen)Paxton
D
16

By default, raising serializers.ValidationError will return with HTTP_400_BAD_REQUEST

But sometimes we would like to return ValidationError with normal 200 status code, because some libraries on the client side can't parse json response data while response code is not 200.

I tried this. but it's not worked:

raise serializers.ValidationError({'message':'Invalid  email address'}, code=200)

So we can do this and it works:

res = serializers.ValidationError({'message':'Invalid  email address'})
res.status_code = 200
raise res
Dogged answered 1/8, 2020 at 23:45 Comment(2)
Wow, I need to test this, it would be great as I encounter the same (the issuer of the POST requests expects 200 status code)Science
This is indeed a pretty clean solution. And yes this also works to raise a 409 conflict.Holmgren
P
3

Use django-rest-framework custom exception handler http://www.django-rest-framework.org/api-guide/exceptions/

def custom_exception_handler(exc, context=None):
    response = exception_handler(exc, context)
    if response is not None:
         if response.data['detail'] == 'Duplicate Username':
            response.data['username'] = response.data.pop('detail')
        response.status_code = status.HTTP_409_CONFLICT
    return response
Pluckless answered 2/11, 2015 at 11:13 Comment(3)
Great, this works. This function will become very complicated if I want to customise the exception for different views. As the number of views increases, this function will grow as well. I wonder if there is a scaleable solution.Pameliapamelina
you mean to override 'detail' ?Pluckless
I mean if there are 3 exceptions that I want to override, then I will have a if..elif..elif structure in custom_exception_handler. The more exception I want to customise, the more ifs I will have to write in this method. Also, I need to remember which if branch is responsible for which view's which HTTP action. This can be a real issue down the line.Pameliapamelina
H
0

To add to Anush Devendra's answer, it seems that raising anything else than a ValidationError will bypass the treatment done by DRF on other fields.

Considering this code from DRF in exceptions.py:

def to_internal_value(self, data):
        [...]

        for field in fields:
            [...]
            try:
                validated_value = field.run_validation(primitive_value)
                if validate_method is not None:
                    validated_value = validate_method(validated_value)
            except ValidationError as exc:
                errors[field.field_name] = exc.detail
            [...]
            else:
                set_value(ret, field.source_attrs, validated_value)

        if errors:
            raise ValidationError(errors)

        return ret

If you want to have this kind of answer:

{
    "my_first_field": [
        "The first field had an error."
    ],
    "my_second_field": [
        "The second field had an error."
    ],
}

you need to raise a ValidationError in the validate_<field>() methods.

Note that doing so you won't be able to have a custom error heriting from ValidationError and with a status_code different of 400. Your detail message will be extract and a new ValidationError (with a default 400 status_code) raise.

Heirloom answered 1/7, 2021 at 12:51 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.