Back to Rules
Django89% popularity

Django REST Framework Serializer Validation

Implement multi-layer validation in DRF serializers for robust API input handling and security.

Tom ChristieUpdated Mar 5, 2024

Overview

Django REST Framework serializers provide a powerful validation system that operates at multiple layers, from field-level validation to object-level validation and even cross-field validation. Understanding this multi-layer approach is essential for building secure APIs that properly handle malicious input. Field-level validation is implemented by adding validate_<field_name> methods to serializer classes. These methods receive the field value and can either return a cleaned value or raise ValidationError. This is the first line of defense, ensuring individual fields meet format and constraint requirements before any object-level processing occurs. Object-level validation in the validate() method operates on the complete validated data after individual field validation has passed. This enables complex business logic that depends on relationships between fields. Common patterns include checking that a end_date is after start_date, verifying user permissions for certain actions, or ensuring uniqueness constraints across related fields. The create() and update() methods in ModelSerializer classes handle the actual persistence of validated data. These methods receive already-validated data, ensuring that only properly validated information reaches the database layer. The validated_data dictionary contains only fields that passed all validation, eliminating the risk of unexpected fields being persisted. DRF's validation system also integrates with Django's permission and authentication classes. The request user is available in serializer context, enabling validation rules that depend on user permissions or roles. For example, a serializer might allow certain field values only for admin users while enforcing stricter limits for regular users.

Code Example

.djangorules
from rest_framework import serializers
from django.contrib.auth import get_user_model
from django.utils import timezone
from .models import Project, Task

User = get_user_model()

class TaskSerializer(serializers.ModelSerializer):
    class Meta:
        model = Task
        fields = ['id', 'title', 'description', 'status', 'assignee', 'due_date', 'project']
        read_only_fields = ['id']

    def validate_title(self, value):
        if len(value) < 3:
            raise serializers.ValidationError("Title must be at least 3 characters.")
        if len(value) > 200:
            raise serializers.ValidationError("Title cannot exceed 200 characters.")
        return value.strip()

    def validate_due_date(self, value):
        if value and value < timezone.now().date():
            raise serializers.ValidationError("Due date cannot be in the past.")
        return value

    def validate(self, attrs):
        if attrs.get('status') == 'completed' and not attrs.get('assignee'):
            raise serializers.ValidationError({
                'assignee': 'Completed tasks must have an assignee.'
            })

        project = attrs.get('project')
        if project and project.status == 'archived':
            raise serializers.ValidationError({
                'project': 'Cannot add tasks to archived projects.'
            })

        return attrs

    def create(self, validated_data):
        validated_data['created_by'] = self.context['request'].user
        return super().create(validated_data)


class ProjectSerializer(serializers.ModelSerializer):
    tasks = TaskSerializer(many=True, read_only=True)
    task_count = serializers.SerializerMethodField()
    completion_rate = serializers.SerializerMethodField()

    class Meta:
        model = Project
        fields = [
            'id', 'name', 'description', 'status', 'owner',
            'created_at', 'updated_at', 'tasks', 'task_count', 'completion_rate'
        ]
        read_only_fields = ['id', 'created_at', 'updated_at', 'owner']

    def get_task_count(self, obj):
        return obj.tasks.count()

    def get_completion_rate(self, obj):
        total = obj.tasks.count()
        if total == 0:
            return 0
        completed = obj.tasks.filter(status='completed').count()
        return round((completed / total) * 100, 2)

More Django Rules

DJANGO
87%

Django QuerySet Optimization

Use select_related, prefetch_related, and only() to minimize database queries.

djangoormqueryset
from django.db.models import Prefetch, Count, Q
from django.shortcuts import render
from .models import Author, Book, Publisher

# Problematic: N+1 qu...
Feb 25, 2024by Aymeric Augustin
View Rule
DJANGO
91%

Django Security Best Practices

Implement comprehensive security measures including CSRF, XSS prevention, and SQL injection protection.

djangosecuritycsrf
from django.http import HttpResponse, HttpResponseForbidden
from django.views.decorators.http import require_http_methods
from django.views.decorators...
Mar 2, 2024by Django Software Foundation
View Rule
DJANGO
89%

Django REST Framework Permissions

Implement granular permission systems with DRF permission classes and custom permission logic.

djangorest-frameworkpermissions
from rest_framework import permissions
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.gener...
Mar 25, 2024by Tom Christie
View Rule