How to Pass Extra Context Data to DRF Serializers in Django

Blog / Django · July 1, 2022 · Updated June 10, 2026 · 6 min read
How to Pass Extra Context Data to DRF Serializers in Django

In Django REST Framework (DRF), every serializer can receive an extra context dictionary, and you read it inside the serializer with self.context. When you use generic views or viewsets, DRF automatically injects three keys — request, view, and format — so your serializers can be request-aware without any extra wiring. To add your own data, override get_serializer_context() in the view (the preferred approach) or pass context={...} when you instantiate a serializer by hand.

This is the focused deep-dive on serializer context. For the broader picture — custom validation, nested write logic, and to_representation mechanics — see our companion guide on customizing Django REST API serializers.

What serializer.context is

context is a plain Python dictionary attached to a serializer instance. Anything you put in it is available on self.context inside fields, method fields, and validators. In generic views and viewsets, DRF builds this dict for you in get_serializer_context():

# rest_framework/generics.py (simplified) - what DRF gives you by default
class GenericAPIView(views.APIView):
    def get_serializer_context(self):
        return {
            'request': self.request,
            'format': self.format_kwarg,
            'view': self,
        }

    def get_serializer(self, *args, **kwargs):
        # context is injected automatically here
        kwargs.setdefault('context', self.get_serializer_context())
        return self.get_serializer_class()(*args, **kwargs)

Because get_serializer() passes that dict in for you, any serializer obtained through self.get_serializer(...) can reach the request with self.context['request']. Viewsets inherit from GenericAPIView, so the same behaviour applies there.

The right way to add custom context

There are two supported ways to inject your own data. In class-based views and viewsets, override get_serializer_context() — this is the cleanest approach because it keeps DRF's defaults and runs for every action:

# views.py - PREFERRED: override get_serializer_context in the view
from rest_framework import generics, viewsets

class SendEmailViewSet(viewsets.GenericViewSet):
    serializer_class = SendEmailSerializer

    def get_serializer_context(self):
        context = super().get_serializer_context()  # keeps request/view/format
        context['exclude_email_list'] = ['spam@example.com', 'blocked@example.com']
        return context


class SendEmailView(generics.GenericAPIView):
    serializer_class = SendEmailSerializer

    def get_serializer_context(self):
        context = super().get_serializer_context()
        context['exclude_email_list'] = ['spam@example.com', 'blocked@example.com']
        return context

In a function-based view (or a bare APIView), nothing is injected automatically — DRF never calls get_serializer_context() for you — so build the dict yourself, including request if your serializer needs it:

# views.py - function-based view: pass context= explicitly
from rest_framework.decorators import api_view
from rest_framework.response import Response
from .serializers import SendEmailSerializer

@api_view(['POST'])
def send_email_view(request):
    context = {
        'request': request,  # FBVs do NOT add this for you
        'exclude_email_list': ['spam@example.com', 'blocked@example.com'],
    }
    serializer = SendEmailSerializer(data=request.data, context=context)
    serializer.is_valid(raise_exception=True)
    # ... send the email ...
    return Response(serializer.validated_data)

Reading context inside fields, methods and validators

Once context is set, read it with self.context. Use .get() with a default so a missing key never crashes the serializer:

from rest_framework import serializers

class SendEmailSerializer(serializers.Serializer):
    email = serializers.EmailField()
    content = serializers.CharField(max_length=200)
    sender = serializers.SerializerMethodField()

    def validate_email(self, value):
        exclude = self.context.get('exclude_email_list', [])
        if value in exclude:
            raise serializers.ValidationError('We cannot send email to this address.')
        return value

    def get_sender(self, obj):
        # SerializerMethodField can read context too
        request = self.context.get('request')
        return request.user.email if request and request.user.is_authenticated else None

    def to_representation(self, instance):
        data = super().to_representation(instance)
        if not self.context.get('include_content', True):
            data.pop('content', None)
        return data

Passing context to nested serializers

When you declare a nested serializer as a field, DRF copies the parent's context down to it automatically — you do nothing, and this includes many=True list fields. Propagation only breaks when you instantiate a serializer manually (for example inside a SerializerMethodField); then you must forward context=self.context yourself.

class AuthorSerializer(serializers.ModelSerializer):
    class Meta:
        model = Author
        fields = ['id', 'name']

class BookSerializer(serializers.ModelSerializer):
    # Declared as a field -> context propagates automatically.
    author = AuthorSerializer(read_only=True)
    related = serializers.SerializerMethodField()

    class Meta:
        model = Book
        fields = ['id', 'title', 'author', 'related']

    def get_related(self, obj):
        qs = obj.related_books.all()
        # Manual instantiation -> you MUST pass context yourself,
        # otherwise self.context['request'] inside the nested serializer is None.
        return BookSerializer(qs, many=True, context=self.context).data

The same rule governs many=True: a declared list field (or a ListSerializer) inherits context automatically, but a hand-built Serializer(queryset, many=True) needs context=self.context or self.context.get('request') will return None.

Request-aware output: absolute URLs and dynamic fields

The most common reason to reach for context is the request. With it you can build absolute URLs and tailor the payload to the current user:

class ProfileSerializer(serializers.ModelSerializer):
    avatar_url = serializers.SerializerMethodField()

    class Meta:
        model = Profile
        fields = ['id', 'name', 'avatar_url', 'internal_notes']

    def get_avatar_url(self, obj):
        request = self.context.get('request')
        if not obj.avatar:
            return None
        url = obj.avatar.url
        # build_absolute_uri turns /media/x.png into https://api.example.com/media/x.png
        return request.build_absolute_uri(url) if request else url

    def to_representation(self, instance):
        data = super().to_representation(instance)
        request = self.context.get('request')
        # Hide a field from non-staff users.
        if not (request and request.user.is_staff):
            data.pop('internal_notes', None)
        return data

Common pitfalls

  • KeyError: 'request' in a script or test. If you instantiate a serializer outside a view — MySerializer(obj) in a shell, a Celery task, or a unit test — no context exists, so self.context['request'] raises KeyError. Pass context={'request': request}, or read defensively with self.context.get('request').
  • self.context.get('request') returns None. Context was never passed down. Check that you didn't bypass get_serializer() by instantiating the serializer manually, and that nested / many=True serializers received context=self.context.
  • Subscripting instead of .get(). self.context['exclude_email_list'] crashes when the key is absent; self.context.get('exclude_email_list', []) degrades gracefully.
  • Forgetting super() in get_serializer_context(). If you return a brand-new dict you drop DRF's request/view/format. Always start from context = super().get_serializer_context().
Do Don't
Override get_serializer_context() in views/viewsets Hard-code request-specific data inside the serializer
Start from super().get_serializer_context() Return a fresh dict that loses request
Read with self.context.get(key, default) Subscript self.context[key] blindly
Pass context=self.context to manual nested serializers Assume context auto-propagates to manual instances
Build context yourself in FBVs, scripts and tests Expect a bare APIView or FBV to inject request

Serializer context is one of those small DRF features that quietly powers clean, request-aware APIs — absolute media URLs, per-user field visibility, multi-tenant scoping, and reusable validators. At MicroPyramid we've shipped 50+ projects over 12+ years building Django and Python REST APIs (and high-throughput FastAPI services), so these context patterns are baked into how we structure serializers and views.

Frequently Asked Questions

What is the serializer context in Django REST Framework?

It's a dictionary attached to a serializer that you read with self.context. DRF's generic views and viewsets auto-populate it with request, view, and format via get_serializer_context(), and you can add your own keys. It lets a serializer make decisions based on the current request without being coupled to the view.

How do I pass custom data to a serializer's context?

Override get_serializer_context() in your view or viewset, call super().get_serializer_context() to keep the defaults, add your keys, and return the dict. When you instantiate a serializer manually (in a function-based view, script, or test), pass context={...} to the constructor instead.

Why is self.context['request'] raising a KeyError?

Because the serializer was created without context — usually because you instantiated it directly instead of going through get_serializer(), or you built a nested / many=True serializer by hand without forwarding context=self.context. Pass the context explicitly, or read defensively with self.context.get('request').

Does context automatically pass to nested serializers?

Yes — when the nested serializer is declared as a field, DRF copies the parent's context to it, including for many=True list fields. It does NOT propagate when you instantiate a nested serializer manually (for example inside a SerializerMethodField); there you must pass context=self.context yourself.

How do I build absolute URLs in a serializer?

Grab the request from context and call request.build_absolute_uri(path). For example self.context['request'].build_absolute_uri(obj.file.url) turns a relative /media/... path into a full https://... URL. Guard for None in case the serializer ever runs without a request.

Can I access the current user inside a serializer?

Yes, through the request in context: self.context.get('request').user. This is how you hide fields from non-staff users, scope querysets to the requesting tenant, or stamp created_by. Always confirm the request exists first, since context-free instantiations (tests, scripts) won't have one.

Should I use get_serializer_context() or pass context manually?

Prefer get_serializer_context() whenever you're in a generic view or viewset — it runs for every action and preserves DRF's defaults. Pass context= manually only when you create a serializer outside that flow: function-based views, management commands, Celery tasks, or unit tests.

Share this article