A neat way to structure your validations in Django REST Framework

A neat way to structure your validations in Django REST Framework

While building APIs, the primary thing to setup is the validations for the API. Validators can be pretty useful for writing validations but sometimes there are a lot of custom validations to be supported.

If you have custom validations, you can override the validate method in your serializer.

class SomeRandomSerializer(serializers.ModelSerializer):

    class Meta:
        model = SomeRandomModel
        fields = [
            'id',
            'some_random_field_1',
            'some_random_field_2',
            'some_random_field_3',
        ]

    def validate(self, data):

        is_field_1_valid = data.get('some_random_field_1') == 100

        if not is_field_1_valid:
            raise serializers.ValidationError({
                'some_random_field_1': [
                    'Some random field 1 should be equal to 100'
                ]
            })

        is_field_2_valid = data.get('some_random_field_2') == 200

        if not is_field_2_valid:
            raise serializers.ValidationError({
                'some_random_field_2': [
                    'Some random field 2 should be equal to 200'
                ]
            })

        return data

    def create(self, validated_data):
        # Instance creation code comes here
        pass

    def update(self, instance, validated_data):
        # Instance updation code comes here
        pass

But the problem writing all your validations here is that if there are a lot of validations, your serializer will become very long. It will be better to move all our validations to a separate file so our serializer file will be clean.

Give me an example

Let's say we are building our Products API and our project's folder structure is shown below.

/ app
    / serializers
        product.py
    / validations
        product.py
    / viewsets
        product.py
    models.py
    urls.py

We have 3 different folders for our serializers, validation files, and viewset files. Hence, we created product.py under each folder.

Our ProductSerializer would look something like this. Check the validation method, we have directly called the validate method from our custom-built ProductValidation class.

# serializers/product.py

from app.models import Product
from app.validations.product import ProductValidation

class ProductSerializer(serializers.ModelSerializer):

    class Meta:
        model = Product
        fields = [
            'id',
            'title',
            'code',
            'description',
            'quantity'
        ]

    def validate(self, data):
        return ProductValidation(
            data=data,
            instance=self.instance).validate()

    def create(self, validated_data):
        # Product creation code comes here
        pass

    def update(self, instance, validated_data):
        # Product updation code comes here
        pass

The ProductValidation Class

class ProductValidation:

    def __init__(self, data, instance):
        self.data = data
        self.instance = instance
        self.is_update_case = instance is not None

    def validate(self):
        if self.is_update_case:
            return self.__validate_UPDATE()
        return self.__validate_CREATE()

    def __validate_CREATE(self):

        errors = {
            **self.__validate_title_length(),
            **self.__validate_code(),
            **self.__validate_description_length(),
            **self.__validate_quantity()
        }
        if errors:
            raise serializers.ValidationError(errors)

        return self.data

    def __validate_UPDATE(self):

        errors = {
            **self.__validate_title_length(),
            **self.__validate_code(),
            **self.__validate_description_length(),
            **self.__validate_quantity()
            **self.__validate_JUST_FOR_UPDATE_CASE_1(),
            **self.__validate_JUST_FOR_UPDATE_CASE_2(),
        }
        if errors:
            raise serializers.ValidationError(errors)

        return self.data

    def __validate_title_length(self):
        errors = dict()
        title = self.data['title']
        if len(title) <= 2:
            errors.update({
                'title': ['Title length should be greater than 2']
            })
        return errors

    def __validate_code(self):
        pass

    def __validate_description_length(self):
        pass

    def __validate_quantity(self):
        pass

    def __validate_JUST_FOR_UPDATE_CASE_1(self):
        pass

    def __validate_JUST_FOR_UPDATE_CASE_2(self):
        pass
  • The constructor of ProductValidation needs two values. The first one is the data passed as in the body of the API and the second is the actual instance.
    • The instance will be None if it is a creation case else there will be the actual instance.
    • Inside the constructor, the is_update_case value is set on the same principle.
  • The validate method calls __validate_UPDATE or the __validate_CREATE method based on the is_update_case boolean.
  • ** can be used inside a dictionary to merge multiple dictionaries.
    • Inside the __validate_CREATE method, we build a dictionary of errors.
    • This dictionary calls multiple validations methods, each of them returning a dictionary of errors (if there are errors else an empty dictionary).
    • If there are any errors, we raise ValidationError passing our errors dictionary.
  • The __validate_UPDATE is the same as the __validate_CREATE method.
    • We created the __validate_UPDATE method because sometimes update APIs have some more validations than the create APIs.

This makes adding new validations, checking for validation errors, or removing any validations very easy. Trust me, if you have tons of validations, this technique makes it very easy to maintain.

Thanks for reading!