Skip to main content

Django REST Validation

In API development, ensuring data quality is crucial. This is where validation comes into play. Django REST Framework (DRF) provides a powerful system for validating incoming data before it's processed or stored. In this tutorial, we'll explore how DRF handles validation and how you can customize it to meet your needs.

Introduction to Validation in DRF

Validation in Django REST Framework ensures that incoming data meets certain criteria before it's accepted. This is essential for:

  • Maintaining data integrity
  • Preventing security vulnerabilities
  • Providing meaningful feedback to API consumers
  • Reducing errors in your application logic

DRF's validation happens primarily at the serializer level, though validation can also occur at different stages of the request-response cycle.

Basic Validation with Serializers

Serializers are the primary place where validation happens in DRF. Let's start with a basic example:

python
from rest_framework import serializers

class UserSerializer(serializers.Serializer):
email = serializers.EmailField()
username = serializers.CharField(max_length=100)
password = serializers.CharField(min_length=8, write_only=True)

In this example:

  • EmailField validates that the input is a properly formatted email
  • CharField with max_length ensures the username isn't too long
  • min_length on password enforces a minimum password length

When you attempt to deserialize data with this serializer, these validations are automatically enforced:

python
serializer = UserSerializer(data={
'email': 'invalid-email',
'username': 'user1',
'password': 'short'
})
serializer.is_valid() # Returns False
print(serializer.errors)

Output:

python
{
'email': ['Enter a valid email address.'],
'password': ['Ensure this field has at least 8 characters.']
}

Field-level Validation

For more complex validation needs, you can implement field-level validation by defining methods following the pattern validate_<field_name>:

python
from rest_framework import serializers

class BlogPostSerializer(serializers.Serializer):
title = serializers.CharField(max_length=100)
content = serializers.CharField()
publish_date = serializers.DateField()

def validate_title(self, value):
"""
Check that the title isn't just whitespace
"""
if value.strip() == '':
raise serializers.ValidationError("Title cannot be empty or whitespace only")
return value

def validate_publish_date(self, value):
"""
Check that publish date is not in the past
"""
import datetime
if value < datetime.date.today():
raise serializers.ValidationError("Publish date cannot be in the past")
return value

These methods receive the field value as an argument, perform validation, and either:

  • Return the validated value (possibly modified)
  • Raise a ValidationError if validation fails

Object-level Validation

Sometimes you need to validate multiple fields together. For this, override the validate() method:

python
class PasswordChangeSerializer(serializers.Serializer):
current_password = serializers.CharField()
new_password = serializers.CharField(min_length=8)
confirm_password = serializers.CharField(min_length=8)

def validate(self, data):
"""
Check that the new passwords match and are different from current
"""
if data['new_password'] != data['confirm_password']:
raise serializers.ValidationError({"confirm_password": "Passwords don't match"})

if data['current_password'] == data['new_password']:
raise serializers.ValidationError(
{"new_password": "New password must be different from current password"}
)

return data

The validate() method receives a dictionary of field values after they've passed their individual validations.

Using Validators

DRF provides several built-in validators that you can use to enforce common constraints:

python
from rest_framework import serializers
from rest_framework.validators import UniqueValidator
from django.contrib.auth.models import User

class RegisterSerializer(serializers.ModelSerializer):
email = serializers.EmailField(
validators=[UniqueValidator(queryset=User.objects.all())]
)
username = serializers.CharField(
validators=[UniqueValidator(queryset=User.objects.all())]
)
password = serializers.CharField(min_length=8, write_only=True)

class Meta:
model = User
fields = ('email', 'username', 'password')

Here, we use UniqueValidator to ensure email and username are unique across all users.

You can also create custom validators as functions:

python
def validate_even_number(value):
if value % 2 != 0:
raise serializers.ValidationError("This field must be an even number.")
return value

class GameScoreSerializer(serializers.Serializer):
score = serializers.IntegerField(validators=[validate_even_number])

Validation in ModelSerializers

ModelSerializer classes automatically generate validators based on your model definition:

python
from rest_framework import serializers
from .models import Product

class ProductModel(models.Model):
name = models.CharField(max_length=100)
price = models.DecimalField(max_digits=10, decimal_places=2)
description = models.TextField()
in_stock = models.BooleanField(default=True)

class Meta:
constraints = [
models.UniqueConstraint(
fields=['name'], name='unique_product_name'
)
]

class ProductSerializer(serializers.ModelSerializer):
class Meta:
model = Product
fields = ['name', 'price', 'description', 'in_stock']

The ProductSerializer will automatically:

  • Apply max_length=100 validation to name
  • Ensure price is a valid decimal with appropriate digits
  • Enforce the unique constraint on name

Error Handling and Response Customization

When validation fails, DRF returns a 400 Bad Request response with the validation errors. You can customize how these errors are formatted in your API responses:

python
# settings.py
REST_FRAMEWORK = {
'NON_FIELD_ERRORS_KEY': 'error',
}

This changes the key used for non-field errors from 'non_field_errors' to 'error'.

For more advanced customization, you can override the to_representation() method of your error response:

python
from rest_framework.views import exception_handler

def custom_exception_handler(exc, context):
# Call REST framework's default exception handler first
response = exception_handler(exc, context)

# Now add custom error formatting
if response is not None:
customized_response = {'status': 'error', 'errors': {}}

for key, value in response.data.items():
error_msg = value[0] if isinstance(value, list) else value
customized_response['errors'][key] = error_msg

response.data = customized_response

return response

# In settings.py:
# REST_FRAMEWORK = {
# 'EXCEPTION_HANDLER': 'path.to.custom_exception_handler'
# }

Real-world Example: Product API with Complex Validation

Let's build a more comprehensive example with a Product API that includes various validation requirements:

python
from rest_framework import serializers, status
from rest_framework.views import APIView
from rest_framework.response import Response
from django.shortcuts import get_object_or_404
from .models import Product, Category

class ProductSerializer(serializers.ModelSerializer):
category_id = serializers.IntegerField(write_only=True)
discount_percent = serializers.IntegerField(required=False, default=0)
final_price = serializers.SerializerMethodField(read_only=True)

class Meta:
model = Product
fields = ['id', 'name', 'description', 'price', 'discount_percent',
'final_price', 'category_id', 'category']
read_only_fields = ['id', 'category']
depth = 1 # This expands the category relationship

def validate_category_id(self, value):
try:
Category.objects.get(id=value)
except Category.DoesNotExist:
raise serializers.ValidationError("Invalid category ID")
return value

def validate_price(self, value):
if value <= 0:
raise serializers.ValidationError("Price must be greater than zero")
return value

def validate_discount_percent(self, value):
if not (0 <= value <= 100):
raise serializers.ValidationError("Discount must be between 0 and 100 percent")
return value

def validate(self, data):
# Additional business logic validation
if 'name' in data and 'description' in data:
if data['name'] in data['description']:
raise serializers.ValidationError({
"description": "Description should not contain the product name"
})
return data

def get_final_price(self, obj):
return obj.price * (1 - (obj.discount_percent / 100))

def create(self, validated_data):
category_id = validated_data.pop('category_id')
category = Category.objects.get(id=category_id)
return Product.objects.create(category=category, **validated_data)

class ProductAPIView(APIView):
def post(self, request):
serializer = ProductSerializer(data=request.data)
if serializer.is_valid():
product = serializer.save()
return Response(
ProductSerializer(product).data,
status=status.HTTP_201_CREATED
)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

In this example:

  1. We validate the category exists
  2. We ensure the price is positive
  3. We ensure the discount is between 0-100%
  4. We have object-level validation ensuring the name isn't in the description
  5. We calculate a derived field based on other validated fields

Summary

Validation is a critical part of building robust APIs with Django REST Framework. We've covered:

  • Basic field validation with serializer fields
  • Field-level validation with validate_<field_name> methods
  • Object-level validation with the validate() method
  • Using and creating validators
  • ModelSerializer automatic validation
  • Error handling and customization
  • Complex validation in a real-world scenario

By implementing proper validation, you ensure your API maintains data integrity while providing clear feedback to consumers when their data doesn't meet your requirements.

Additional Resources

  1. DRF Validation Documentation
  2. Django Model Validation
  3. Django Built-in Validators

Exercises

  1. Create a serializer for a Book model that validates:

    • ISBN matches the standard ISBN-13 format
    • Publication date is not in the future
    • Price is a positive number
  2. Implement custom validation for a UserProfile serializer that ensures:

    • Users must be at least 18 years old (based on date of birth)
    • Phone numbers follow a specific format
    • If a user is marked as a "premium" member, they must provide a payment method
  3. Create a custom validator function that checks if a URL is accessible (returns a 200 status code)



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)