How are all serializer errors returned in DRF at once?

Viewed 788

I'm testing for multiple validation errors to be raised in(UserRegistrationSerializer). Yet DRF only returns the first error that is raised: {'username': [ErrorDetail(string='Choose a different username', code='invalid')]}

I'm expecting: {'username': [ErrorDetail(string='Choose a different username', code='invalid')], 'password2': [ErrorDetail(string='Password confirmation failed', code='invalid')]

How can multiple errors be accounted for once a serializer is validated as in the documentation example?

https://www.django-rest-framework.org/api-guide/serializers/#validation

class TestRegisterationSerializer__002(TestCase):
    '''Verify that the registeration process fails with respect
    to selecting an unavailable username and password confirmation'''

    @classmethod
    def setUpTestData(cls):
        User.objects.create_user(username="Python")
        cls.data = {
            'username': "Python",
            'password': "#secret#",
            'password2': "Secret"
        }
        cls.error_messages = [
            "Choose a different username", "Password confirmation failed"
        ]
        cls.serializer = UserRegisterationSerializer(data=cls.data)

    def test_user_registeration_invalid_confirmation(self):
        self.serializer.is_valid()
        print(self.serializer.errors)
import re

from django.contrib.auth.models import User

from rest_framework import serializers


class UsernameSerializer(serializers.ModelSerializer):

    username = serializers.SlugField(min_length=4, max_length=12)

    def validate_username(self, value):
        try:
            self.Meta.model.objects.get(username__iexact=value)
        except self.Meta.model.DoesNotExist:
            return value
        raise serializers.ValidationError("Choose a different username")

    class Meta:
        fields = ['username', ]
        model = User


class LoginSerializer(UsernameSerializer):

    password = serializers.RegexField(
        r"[0-9A-Za-z]+", min_length=5, max_length=8
    )

    def validate(self, data):
        username, password = [
            input.lower() for input in [data['username'], data['password']]
        ]
        if all(password[i] == password[i + 1] for i in range(len(password) - 1)) or username == password:
            raise serializers.ValidationError({
                'password': "Invalid password"
            })
        return data

    class Meta:
        fields = ['username', 'password', ]
        model = User


class UserRegistrationSerializer(LoginSerializer):

    password2 = serializers.RegexField(
        r"[0-9A-Za-z]+", min_length=5, max_length=8, write_only=True
    )

    def validate(self, data):
        data = super().validate(data)
        if data['password'] != data['password2']:
            raise serializers.ValidationError({"password2": "Password confirmation failed"})
        return data

    class Meta:
        model = User
        fields = ['username', 'password', 'password2']


2 Answers

Please remove the customizations of validate method in UsernameSerializer and LoginSerializer and place this method inside UserRegistrationSerializer

from rest_framework.exceptions import ValidationError

def validate(self, data):
    errors = []
    data = super().validate(data)
    username = data.get("username")
    if self.Meta.model.objects.filter(username__iexact=username).exists():
       errors.append({"username":"Choose a different username"})

    username, password = [
            input.lower() for input in [data['username'], data['password']]
        ]
    if all(password[i] == password[i + 1] for i in range(len(password) - 1)) or username == password:
        errors.append({
            'password': "Invalid password"
        })
    
    if data['password'] != data['password2']:
        errors.append({"password2": "Password confirmation failed"})

    if errors:
        raise serializers.ValidationError(errors)
    return data

As Krishna mentioned, you should use the validate method of the UserRegistrationSerializer - it should be in the validate() method rather than validate_<param>() methods because it is a validation across fields, rather than a validation of a single field.

However Krishna's raised error will not work using django 4.0.6 (for me, at least!) - the reason is that the errors object needs to be a dictionary with the fields as keys, not a list of dictionaries. Passing a list of dictionaries will cause the errors to be represented as non_field_errors.

Krishna's corrected example would then be:

from rest_framework import serializers

class UserRegistrationSerializer(serializers.ModelSerializer):
    ...other specs ...
    def validate(self, data):
        errors = {}
        username = data.get("username")
        if self.Meta.model.objects.filter(username__iexact=username).exists():
           errors["username"] ="Choose a different username"

        username, password = [
            input.lower() for input in [data['username'], data['password']]
        ]
        if all(password[i] == password[i + 1] for i in range(len(password) - 1)) or username == password:
            errors['password'] = "Invalid password"
    
        if data['password'] != data['password2']:
           errors["password2"] ="Password confirmation failed"

        if errors:
            raise serializers.ValidationError(errors)
        return data

This will raise the errors correctly as individual field errors in django 4.0.6

Related