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:
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 emailCharField
withmax_length
ensures the username isn't too longmin_length
on password enforces a minimum password length
When you attempt to deserialize data with this serializer, these validations are automatically enforced:
serializer = UserSerializer(data={
'email': 'invalid-email',
'username': 'user1',
'password': 'short'
})
serializer.is_valid() # Returns False
print(serializer.errors)
Output:
{
'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>
:
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:
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:
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:
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:
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:
# 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:
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:
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:
- We validate the category exists
- We ensure the price is positive
- We ensure the discount is between 0-100%
- We have object-level validation ensuring the name isn't in the description
- 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
Exercises
-
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
-
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
-
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! :)