Search code examples
djangodjango-rest-frameworkdjango-serializer

Validate Unique email django rest framework serializer


I have a model where Email field is unique. I thought it would make every email lowercase as it has normalize_email() method on User instance. However, it normalizes domain part only so Company@gmail.com is considered to be unique if company@gmail.com exists. So I decided to create validate_email() in my serializer to always return a lowercase email. It is a field validation and it is described in docs.

    def validate_email(self, value):
        return value.lower()

However, looks like this method returns the value after the serializer has checked if the value exists in the database. Here is an example:

if I try to create a user with user@gmail.com and it already exists, it will return "user already exists" which is expected. However, if I run with User@gmail.com it will run a SQL request first and check User@gmail.com != user@gmail.com and after that it'll try to create a new instance with user@gmail.com because it is returned from validate_email() and IntegrityError will be raised because it becomes user@gmail.com which is already in DB!

I could do something like

    def validate_email(self, value):
        norm_email = value.lower()
        if User.objects.filter(email=norm_email).exists():
            raise serializers.ValidationError("Not unique email")
        return norm_email

but this is another request to DB and I don't want it.

So my question is what method runs a SQL request to check uniqueness in DB? So that I can override it and pass value already lowercased?


Solution

  • Use iexact--(django doc) lookup

    User.objects.filter(email__iexact=norm_email).exists()

    Update

    DRF querying under the hood to check the unique constraint because in model field we have set unique=True. In order to avoid that, we need to define the email field explicitly in the serializer, which bypasses the unique check validation

    class UserSerializer(serializers.ModelSerializer):
        email = serializers.EmailField()
    
        def validate_email(self, value):
            lower_email = value.lower()
            if User.objects.filter(email__iexact=lower_email).exists():
                raise serializers.ValidationError("Duplicate")
            return lower_email
    
        class Meta:
            model = User
            fields = ('email',)

    Django shell output

    In [17]:  print(len(connection.queries))
    7
    
    In [18]: class UserSerializer(serializers.ModelSerializer):
        ...:     email = serializers.EmailField()
        ...: 
        ...:     def validate_email(self, value):
        ...:         lower_email = value.lower()
        ...:         if User.objects.filter(email__iexact=lower_email).exists():
        ...:             raise serializers.ValidationError("Duplicate")
        ...:         return lower_email
        ...: 
        ...:     class Meta:
        ...:         model = User
        ...:         fields = ('email',)
        ...: 
    
    In [19]: print(len(connection.queries))
    7
    
    In [20]: s = UserSerializer(data={'email': 'Foo@gmail.com'})
    
    In [21]: print(len(connection.queries))
    7
    
    In [22]: try:
        ...:     s.is_valid(True)
        ...: except serializers.ValidationError:
        ...:     print("raised validation error")
        ...: 
    raised validation error
    
    In [23]: print(len(connection.queries))
    8