Jinto Antony
Jinto Antony

Reputation: 468

Skipping django serializer unique validation on update for the same row

I am using django serializer for validation. The code is listed below. The same code is used for validation checks on creation and updation. However, on the update, the unique validation check on certain fields should be skipped (eg: email). because on update the row will be used.

from rest_framework import serializers
from rest_framework.validators import UniqueValidator, UniqueTogetherValidator
from dashboard.models import Users

class UserSerializer(serializers.Serializer):
    username = serializers.CharField(max_length=200, required=True, validators=[UniqueValidator(queryset=Users.objects.all())])
    email = serializers.EmailField(validators=[UniqueValidator(queryset=Users.objects.all())])

    def validate_username(self, value):
        if len(value) < 6:
            raise serializers.ValidationError('Username must have at least 3 characters')
        return value

   def validate_email(self, value):
        if len(value) < 3:
            raise serializers.ValidationError('Username must have at least 3 characters')
        return value

def validate(self, data):
    return data

Here, I am using the UniqueValidator, it should be skipped for the update validation check, except for the same row.

Upvotes: 0

Views: 2460

Answers (3)

SAAN07
SAAN07

Reputation: 23

I suppose this is a model related task. So, I am using a ModelSerializer instead of a normal Serializer. Basically the create vs update and it's corresponding validations are deterred by the serializer based on one attribute. The name of the attribute is instance. If this attribute is None the serializer thinks it is a new value so create operation is performed and if the value is not none and the value of instance is a valid model object then the serializer perform update operation.

Here, one thing to note that the corresponding validations are also run based on the Create or Update operation.

For a situation like yours all you need to do is to populate the instance attribute value with a correct model object. Here is the constructor method of the DRF BaseSerializer class.

def __init__(self, instance=None, data=empty, **kwargs):
    self.instance = instance
    if data is not empty:
        self.initial_data = data
    self.partial = kwargs.pop('partial', False)
    self._context = kwargs.pop('context', {})
    kwargs.pop('many', None)
    super().__init__(**kwargs)

Here, you can easily see that when your views call the serializer class if you put some value on the instance parameter it will populate the instance attribute and then the serializer will perform the update related operations. If you do not put any value on instance parameter then the serializer will perform create related operations.

For your better understanding, here I am giving you a shot of DRF ModelViewset's create() and update() method. Look how the serializer class binding is different here.

The create() method of DRF ModelViewset

def create(self, request, *args, **kwargs):
    serializer = self.get_serializer(data=request.data)
    serializer.is_valid(raise_exception=True)
    self.perform_create(serializer)
    headers = self.get_success_headers(serializer.data)
    return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)

And the update() method of DRF ModelViewset

def update(self, request, *args, **kwargs):
    partial = kwargs.pop('partial', False)
    instance = self.get_object()
    serializer = self.get_serializer(instance, data=request.data, partial=partial)
    serializer.is_valid(raise_exception=True)
    self.perform_update(serializer)

    if getattr(instance, '_prefetched_objects_cache', None):
        # If 'prefetch_related' has been applied to a queryset, we need to
        # forcibly invalidate the prefetch cache on the instance.
        instance._prefetched_objects_cache = {}

    return Response(serializer.data)

Look at the 1st line of the create() method and the 3rd line of the update() method carefully.

Here on the create() method the serializer class is instantiate only with the request.data and nothing is sent on the instance parameter. Hence the instance attribute gets None and the serializer is performing create related operations.

And on the update() method the serializer class is instantiate with a variable called instance along with the request.data. And check on update() method line no. 2 how the update method is getting the instance variable value. So, now the serializer class instance attribute is not None so, it is performing update related operations.

Here the value of the instance must be a valid model object.

Now summing together all of these concepts. I am assuming your API is a POST request API which will create a new instance if no other instance checked by your email exists on the table and if any instance has found with this email address then it will perform an update operation.

def create(self, request, *args, **kwargs):
    try:
        instance = self.model.objects.get(email=request.data['email'])
        serializer = self.get_serializer(instance, data=request.data, partial=False)
    except self.model.DoesNotExist:
        serializer = self.get_serializer(data=request.data)
    serializer.is_valid(raise_exception=True)
    self.perform_create(serializer)
    headers = self.get_success_headers(serializer.data)
    return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)

Here, you can see that if an instance with the email address is found then it will update it and if not found then it will create a new one.

Warning: Accessing raw request body data is dangerous. To make it safe you can do a vaild email check manually before calling the database.

More more information check the Github source codes of DRF package.

Upvotes: 1

Ali Aref
Ali Aref

Reputation: 2412

while using the serializers.Serializer

you can set unique=False as email = serializers.EmailField(unique=False) then implement your own logic as

I was coding something like this (hadn't test this yet)

def validate(self, data):
    email = data.get("email")
    username = data.get("username")

    # checking if it's update
    is_update = User.objects.filter(username=username)
    if not is_update:
        # cheking if email exists
        is_unique = User.objects.filter(email=email).exists()
        if len(email) < 6:
            raise serializers.ValidationError("The email len should be greater than 6 char.")
        elif not is_unique:
            raise serializers.ValidationError(f"The e-mail address {email} is already being used")

    if len(username) < 6:
        raise serializers.ValidationError("Username must have at least 3 characters")

    return data

Upvotes: 1

Ashraful Islam
Ashraful Islam

Reputation: 571

You should write another serialzier for Update operation.

Upvotes: 0

Related Questions