A neat way to structure your validations in Django REST Framework

Search for a command to run...

No comments yet. Be the first to comment.
Developers often tell me that the requirements they receive are usually vague and unclear. Another common complaint is that the requirements change midway, causing them to get stuck. Additionally, sometimes the deadline is set before the requirements...

Imagine you've got a REST API for listing users, and each user's information shows up in 12 fields. GET /v1/users/?page=1&page_size=15 { "pagination_data": { "total_records": 10000, "pages": 667, "prev_page_no": null, ...

Decoupling the Data Layer for Enhanced Code Modularity

Essentials for Safeguarding and Maintaining Your Sprint Cycles

A Step-by-Step Tutorial with Practical Examples

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.
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
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
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.is_update_case value is set on the same principle.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.__validate_CREATE method, we build a dictionary of errors.ValidationError passing our errors dictionary.__validate_UPDATE is the same as the __validate_CREATE method.__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!