backend / web frameworks / django / 05_serializers_actions.md

Django Serializers and Actions Guide

3 interview angles 8 min read source

Django Serializers and Actions Guide

Table of Contents

  1. Django Serializers

  2. Django REST Framework Actions

Django Serializers

Introduction to Serializers

Serializers in Django, specifically in Django REST Framework (DRF), are responsible for converting complex data types such as Django models or querysets into Python native data types that can be easily rendered into JSON, XML, or other content types. Serializers also provide deserialization, allowing parsed data to be converted back into complex types after validating the incoming data.

Serializers act as a two-way conversion tool:

  • Serialization: Convert Django model instances → Python native datatypes → JSON/XML
  • Deserialization: Convert JSON/XML → Python native datatypes → Django model instances
   Database Objects                Native Python Types              JSON/XML
        (Models)                     (dict, list, etc.)
       ┌─────────┐                     ┌─────────┐                 ┌─────────┐
       │         │   Serialization     │         │                 │         │
       │ Django  │ ────────────────>   │ Python  │ ────────────>   │ API     │
       │ Models  │                     │ Objects │                 │ Response│
       │         │   <────────────     │         │   <────────     │         │
       └─────────┘   Deserialization   └─────────┘                 └─────────┘

Types of Serializers

Django REST Framework offers several types of serializers:

  1. Serializer: The base serializer class that provides serialization and deserialization functionality.

  2. ModelSerializer: A shortcut that automatically creates a set of fields and validators based on a Django model.

  3. HyperlinkedModelSerializer: Similar to ModelSerializer but uses hyperlinks to represent relationships instead of primary keys.

  4. ListSerializer: Handles serializing multiple objects at once.

Creating Serializers

Basic Serializer Example

from rest_framework import serializers

class PersonSerializer(serializers.Serializer):
    id = serializers.IntegerField(read_only=True)
    name = serializers.CharField(max_length=100)
    age = serializers.IntegerField()
    email = serializers.EmailField()
    
    def create(self, validated_data):
        return Person.objects.create(**validated_data)
    
    def update(self, instance, validated_data):
        instance.name = validated_data.get('name', instance.name)
        instance.age = validated_data.get('age', instance.age)
        instance.email = validated_data.get('email', instance.email)
        instance.save()
        return instance

ModelSerializer Example

from rest_framework import serializers
from .models import Person

class PersonModelSerializer(serializers.ModelSerializer):
    class Meta:
        model = Person
        fields = ['id', 'name', 'age', 'email']
        # Alternatively, use fields = '__all__' to include all fields
        # Or use exclude = ['field_name'] to exclude specific fields

Serializer Fields

Django REST Framework provides a wide range of field types:

  • Basic fields: CharField, IntegerField, BooleanField, DateTimeField, etc.
  • Relationship fields: RelatedField, PrimaryKeyRelatedField, HyperlinkedRelatedField, etc.
  • Composite fields: ListField, DictField, JSONField, etc.
  • Custom fields: Created by subclassing serializers.Field
class ExampleSerializer(serializers.Serializer):
    name = serializers.CharField(max_length=100)
    is_active = serializers.BooleanField(default=True)
    joined_date = serializers.DateTimeField()
    profile_image = serializers.ImageField(required=False)
    tags = serializers.ListField(child=serializers.CharField())
    metadata = serializers.JSONField(required=False)

Validation

Serializers provide multiple levels of validation:

  1. Field-level validation:
def validate_name(self, value):
    if len(value) < 2:
        raise serializers.ValidationError("Name must be at least 2 characters long")
    return value
  1. Object-level validation:
def validate(self, data):
    if data['start_date'] > data['end_date']:
        raise serializers.ValidationError("End date must be after start date")
    return data
  1. Custom validators:
from rest_framework import serializers

def is_future_date(value):
    if value < timezone.now().date():
        raise serializers.ValidationError("Date must be in the future")

class EventSerializer(serializers.ModelSerializer):
    event_date = serializers.DateField(validators=[is_future_date])
    
    class Meta:
        model = Event
        fields = '__all__'

Nested Serializers

Serializers can be nested to represent related objects:

class CommentSerializer(serializers.ModelSerializer):
    class Meta:
        model = Comment
        fields = ['id', 'content', 'created_at']

class PostSerializer(serializers.ModelSerializer):
    # Nested relationship
    comments = CommentSerializer(many=True, read_only=True)
    
    class Meta:
        model = Post
        fields = ['id', 'title', 'content', 'created_at', 'comments']

For writable nested serializers, you’ll need to override the create() and update() methods:

def create(self, validated_data):
    comments_data = validated_data.pop('comments', [])
    post = Post.objects.create(**validated_data)
    
    for comment_data in comments_data:
        Comment.objects.create(post=post, **comment_data)
    
    return post

SerializerMethodField

This field allows custom methods to provide data for serialization:

class UserSerializer(serializers.ModelSerializer):
    full_name = serializers.SerializerMethodField()
    days_since_joined = serializers.SerializerMethodField()
    
    class Meta:
        model = User
        fields = ['id', 'username', 'email', 'full_name', 'days_since_joined']
    
    def get_full_name(self, obj):
        return f"{obj.first_name} {obj.last_name}"
    
    def get_days_since_joined(self, obj):
        return (timezone.now().date() - obj.date_joined.date()).days

Custom Serializer Fields

You can create custom fields by subclassing serializers.Field:

class ColorField(serializers.Field):
    def to_representation(self, value):
        # Convert the RGB value to a hex string
        return '#{:02x}{:02x}{:02x}'.format(value.red, value.green, value.blue)
    
    def to_internal_value(self, data):
        # Convert the hex string to RGB values
        import re
        match = re.match(r'^#([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$', data)
        if not match:
            raise serializers.ValidationError("Invalid hex color format")
        
        return {
            'red': int(match.group(1), 16),
            'green': int(match.group(2), 16),
            'blue': int(match.group(3), 16)
        }

Performance Considerations

For large querysets, consider:

  1. Prefetching related data:
queryset = Post.objects.prefetch_related('comments').all()
serializer = PostSerializer(queryset, many=True)
  1. Using select_related() for ForeignKey relationships:
queryset = Comment.objects.select_related('author').all()
  1. Using pagination:
from rest_framework.pagination import PageNumberPagination

class StandardResultsSetPagination(PageNumberPagination):
    page_size = 100
    page_size_query_param = 'page_size'
    max_page_size = 1000

Django REST Framework Actions

Introduction to Actions

Actions in Django REST Framework are methods that get mapped to specific URLs in your API. They allow you to define custom endpoints beyond the standard CRUD operations provided by ViewSets. Actions are particularly useful for implementing business logic or non-CRUD operations.

Actions are typically defined in ViewSets and are mapped to specific HTTP methods and URL patterns.

Standard Actions

Django REST Framework’s ViewSets come with standard actions:

Method Name HTTP Method URL Pattern Purpose
list GET /resources/ Get multiple resources
create POST /resources/ Create a new resource
retrieve GET /resources/{id}/ Get a single resource
update PUT /resources/{id}/ Full update of a resource
partial_update PATCH /resources/{id}/ Partial update of a resource
destroy DELETE /resources/{id}/ Delete a resource
from rest_framework import viewsets
from .models import Book
from .serializers import BookSerializer

class BookViewSet(viewsets.ModelViewSet):
    queryset = Book.objects.all()
    serializer_class = BookSerializer
    
    # Example of overriding a standard action
    def list(self, request):
        # Custom implementation of list action
        queryset = self.filter_queryset(self.get_queryset())
        serializer = self.get_serializer(queryset, many=True)
        return Response(serializer.data)

Custom Actions

Custom actions extend the functionality of ViewSets beyond CRUD operations:

from rest_framework import viewsets, status
from rest_framework.decorators import action
from rest_framework.response import Response

class BookViewSet(viewsets.ModelViewSet):
    queryset = Book.objects.all()
    serializer_class = BookSerializer
    
    @action(detail=True, methods=['post'])
    def mark_as_read(self, request, pk=None):
        book = self.get_object()
        book.is_read = True
        book.save()
        return Response({'status': 'book marked as read'})
    
    @action(detail=False, methods=['get'])
    def bestsellers(self, request):
        bestsellers = Book.objects.filter(is_bestseller=True)
        serializer = self.get_serializer(bestsellers, many=True)
        return Response(serializer.data)

Action Decorators

The @action decorator accepts several parameters:

  • detail: Boolean indicating if the action is performed on a single instance (True) or on the entire collection (False)
  • methods: List of HTTP methods this action responds to
  • url_path: Custom URL segment (defaults to the function name)
  • url_name: Name for the URL pattern (for reverse resolution)
  • permission_classes: Override viewset permissions for this action
  • throttle_classes: Override viewset throttling for this action
  • serializer_class: Override viewset serializer for this action
  • parser_classes, renderer_classes: Override input/output formats
@action(
    detail=True, 
    methods=['post'], 
    url_path='mark-read', 
    url_name='mark_as_read',
    permission_classes=[IsAuthenticated, IsBookOwner],
    serializer_class=BookStatusSerializer
)
def mark_as_read(self, request, pk=None):
    # Implementation...

Routing Actions

Actions are automatically routed by the Django REST Framework router:

from rest_framework.routers import DefaultRouter
from .views import BookViewSet

router = DefaultRouter()
router.register(r'books', BookViewSet)

urlpatterns = [
    path('api/', include(router.urls)),
]

This creates the following URL patterns:

  • /api/books/ - list, create
  • /api/books/{pk}/ - retrieve, update, partial_update, destroy
  • /api/books/{pk}/mark-as-read/ - custom detail action
  • /api/books/bestsellers/ - custom list action

Permissions and Actions

You can specify different permissions for different actions:

from rest_framework import permissions

class BookViewSet(viewsets.ModelViewSet):
    # Default permissions
    permission_classes = [permissions.IsAuthenticated]
    
    @action(detail=True, methods=['post'], permission_classes=[permissions.IsAdminUser])
    def feature(self, request, pk=None):
        # Only admins can feature a book
        book = self.get_object()
        book.is_featured = True
        book.save()
        return Response({'status': 'book featured'})

Nested Actions

Actions can be nested to represent relationships between resources:

class AuthorViewSet(viewsets.ModelViewSet):
    queryset = Author.objects.all()
    serializer_class = AuthorSerializer
    
    @action(detail=True, methods=['get'])
    def books(self, request, pk=None):
        author = self.get_object()
        books = Book.objects.filter(author=author)
        serializer = BookSerializer(books, many=True)
        return Response(serializer.data)
    
    @action(detail=True, methods=['post'])
    def add_book(self, request, pk=None):
        author = self.get_object()
        serializer = BookSerializer(data=request.data)
        if serializer.is_valid():
            serializer.save(author=author)
            return Response(serializer.data, status=status.HTTP_201_CREATED)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

Action Response Formats

Actions can return various types of responses:

@action(detail=True, methods=['get'])
def download_pdf(self, request, pk=None):
    book = self.get_object()
    # Generate PDF content
    pdf_content = generate_pdf(book)
    
    response = HttpResponse(pdf_content, content_type='application/pdf')
    response['Content-Disposition'] = f'attachment; filename="{book.title}.pdf"'
    return response

@action(detail=True, methods=['get'])
def statistics(self, request, pk=None):
    book = self.get_object()
    stats = {
        'views': book.view_count,
        'downloads': book.download_count,
        'ratings_avg': book.ratings.aggregate(Avg('score'))['score__avg'],
    }
    return Response(stats)

Best Practices

  1. Use meaningful action names that describe what the action does.

  2. Group related actions logically within the same ViewSet.

  3. Keep actions focused on a specific task or functionality.

  4. Use appropriate HTTP methods for different operations:

    • GET for retrieving information
    • POST for creating or processing
    • PUT/PATCH for updates
    • DELETE for removal
  5. Document your actions using DRF’s built-in documentation tools:

@action(detail=True, methods=['post'])
def mark_as_read(self, request, pk=None):
    """
    Mark a book as read by the current user.
    
    This endpoint updates the book's status for the requesting user
    and records the completion date.
    """
    # Implementation...
  1. Provide consistent error responses:
@action(detail=True, methods=['post'])
def rate(self, request, pk=None):
    try:
        score = int(request.data.get('score', 0))
        if not 1 <= score <= 5:
            return Response(
                {'error': 'Score must be between 1 and 5'}, 
                status=status.HTTP_400_BAD_REQUEST
            )
        
        book = self.get_object()
        # Implementation...
        
    except ValueError:
        return Response(
            {'error': 'Invalid score format'}, 
            status=status.HTTP_400_BAD_REQUEST
        )
  1. Use serializers for input validation even in custom actions.

  2. Consider caching for frequently accessed actions that don’t change often.

Conclusion

Serializers and actions are two fundamental concepts in Django REST Framework that work together to create robust and flexible APIs:

  • Serializers handle the conversion between Django models and Python/JSON data types, along with validation.
  • Actions provide a way to expose custom endpoints and business logic in your API.

By mastering these two concepts, you can build powerful, RESTful APIs that go beyond simple CRUD operations while maintaining clean, maintainable code.

Interview angle

  • “What does a DRF serializer do?” - validation and conversion in both directions: parsing and validating incoming data, and rendering model instances to primitives. It’s the boundary layer, and it’s where input validation belongs.
  • “Where do you put validation?” - validate_<field> for one field, validate for cross-field rules. Business invariants that outlive the API belong in the model or a service, not only in the serializer.
  • “How do you avoid N+1 in a list endpoint?” - select_related for forward FK and one-to-one, prefetch_related for reverse and many-to-many, applied in the viewset’s queryset. Nested serializers without them are the classic cause.