How Model Validation Works in Django REST Framework

How Model Validation Works in Django REST Framework

When working with models Django handles most of the validation. This is because Django's built-in validators can do most of the things you want. In a traditional Django application, validation is triggered when you use a ModelForm. Most of the time you will put all of your custom validation logic inside your ModelForm. The call to a form's is_valid() method performs the validation process. I wrote about how this process works in this article, so I'll not be going over it in this article.

In this article, I will show you how validation works in Django REST Framework (DRF).

Data Validation in DRF

With ModelForm the validation is performed partially on the form, and partially on the model instance. With REST framework the validation is performed entirely on the serializer class.

- DRF Documentation

We'll use the following model to demonstrate how validation works in DRF:

class Student(models.Model):
    name = models.CharField(max_length=50)
    age = models.IntegerField()
    email = models.EmailField()

    def __str__(self):
        return self.name

The Student model is a very simple model with only a few attributes. Since all validation is performed on the serializer class, let's create a serializer for our Student Model:

#serializers.py

from rest_framework import serializers
from .models import Student


class StudentSerializer(serializers.ModelSerializer):

    class Meta:
        model = Student
        fields = ['name', 'age', 'email']

In DRF, validation can be performed at the field and class level.

Field Level Validation

Field-level validation can be performed in the following ways:

  • passing a validator directly to a field. Just like Model and ModelForm fields, serializer fields also take a validators keyword and a list of validators as its argument:

    name = serializers.CharField(validators=[name_is_capitalized])

  • creating a custom validator for the field by writing a method in the serializer whose name takes the form validate_<field_name>. DRF will call this method when it's running the validators of the serializer:

      # serializers.py
    
      class StudentSerializer(serializers.ModelSerializer):
          def validate_name(self, value):
               if value[0].islower():
                    raise serializers.ValidationError('Name must be capitalized.')
               return value
    
          class Meta:
              model = Student
              fields = ['name', 'age', 'email']
    

Object Level Validation

Object-level validation is usually done for validation that spans across multiple fields or for performing certain actions before saving or updating the database. It can be achieved in a number of ways by overriding the serializer's:

  • validate() method

  • create() method

  • update() method

Validate() Method

The validate() method is passed a dictionary of data and should return a dictionary of validated data or raises a ValidationError if the check fails. Let's say we want to force minors to only use a Gmail account when signing up:

# serializers.py

class StudentSerializer(serializers.ModelSerializer):
    if data['age']<18 and data['email'].split('@')[1] != 'gmail.com':
            raise serializers.ValidationError(
                {'email': 'Minors must use gmail.com'}
            )
        return data

    class Meta:
        model = Student
        fields = ['name', 'age', 'email']

Create() and Update() Method

Sometimes you want to perform the validation logic inside the Create() or Update() method before saving to the database. In most cases, having just a validate() method is sufficient. But there are scenarios where you may only want to perform certain validations when a user is updating their information.

Let's say users were allowed to change their age but the new age cannot be lower than the previous one. This validation logic is best suited for the update() method because putting this logic inside the validate() method will be problematic when a user is trying to create a student account instead of updating one. This makes sense because unlike the validate() method—which is run every time the serializer is called—the update() method only runs when you are trying to update or change a student's information.

class StudentSerializer(serializers.ModelSerializer):   
    def update(self, instance, validated_data):
        if validated_data['age'] < instance.age:
            raise serializers.ValidationError(
                {'age': 'Age cannot be reduced.'}
            )
        instance.name = validated_data.get('name', instance.name)
        instance.age = validated_data.get('age', instance.age)
        instance.email = validated_data.get('email', instance.email)
        instance.save()
        return instance

    class Meta:
        model = Student
        fields = ['name', 'age', 'email']

Conclusion

I showed you how to perform validation in DRF at both the field and object levels. There are other hacks you can implement to perform validation but they may require more work and may present you with unnecessary bugs or security issues. The DRF documentation recommends putting all your data validation inside your serializer and that's what we did here. Putting validation logic elsewhere may require handling error responses, as Django spits out a bunch of HTML instead of a JSON response which may not be what you want for a REST application.