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?

5 Answers 5

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)
Sign up to request clarification or add additional context in comments.

2 Comments

ternary operator may be cleaner here, self.status_code = status_code if status_code else status.HTTP_500_INTERNAL_SERVER_ERROR
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)
15

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

3 Comments

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)
This is indeed a pretty clean solution. And yes this also works to raise a 409 conflict.
It doesn't work, at least with django 4.2.18/DRF 3.15.2. Unless I'm somehow screwing it up with custom middleware.
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

3 Comments

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.
you mean to override 'detail' ?
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.
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.

Comments

0

Here is a function called create_exception that defines a new exception and returns it. The effect on performance is negligible and provides simplicity to change a status instead of having to redeclare imports or rename exceptions.

from rest_framework import exceptions

def snake_it(string: str):
    return string.replace(" ", "_").lower()


def create_exception(msg: str, code: int):
    class CustomException(exceptions.APIException):
        status_code = code
        default_detail = msg
        default_code = snake_it(msg)

    return CustomException()

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.