'context in nested serializers django rest framework

If i have a nested serializer:

class ChildSerializer(ModelSerializer):
    class Meta:
        fields = ('c_name', )
        model = Child


class ParentSerializer(ModelSerializer):

    child = ChildSerializer(many=True, read_only=True)

    class Meta:
        model = Parent
        fields = ('p_name', 'child')

And i want to access the context in the nested serializer, how can i do that? As far as i can tell, context isn't passed to the Child.

I want to be able to implement a permission model per user on fields, for that i overridden the get_fields() method of the ModelSerializer:

def get_fields(self):
    fields = super().get_fields()
    ....
    for f in fields:
        if has_rights(self.context['request'].user, f, "read"):
            ret_val[f] = fields[f]
    ....
    return ret_val

Which works for regular serializers, but the context, and thus the request and user are not available when the nested child is passed to get_fields(). How do i access the context when the serializer is nested?



Solution 1:[1]

Ok i found a working solution. I replaced the ChildSerializer assignment in the Parent class with a SerializerMethodField which adds the context. This is then passed to the get_fields method in my CustomModelSerializer:

class ChildSerializer(CustomModelSerializer):
    class Meta:
        fields = ('c_name', )
        model = Child


class ParentSerializer(CustomModelSerializer):

    child = serializers.SerializerMethodField('get_child_serializer')

    class Meta:
        model = Parent
        fields = ('p_name', 'child')

    def get_child_serializer(self, obj):
        serializer_context = {'request': self.context.get('request') }
        children = Child.objects.all().filter(parent=obj)
        serializer = ChildSerializer(children, many=True, context=serializer_context)
        return serializer.data

and in my CustomModelSerializer:

class CustomModelSerializer(rest_serializer_classes.HyperlinkedModelSerializer):

    def __init__(self, *args, **kwargs):
        """
            Make sure a user is coupled to the serializer (needed for permissions)
        """
        super().__init__(*args, **kwargs)
        if not self.context:
            self._context = getattr(self.Meta, 'context', {})
        try:
            self.user = self.context['request'].user
        except KeyError:
            self.user = None


    def get_fields(self):
        ret = OrderedDict()

        if not self.user:
            print("No user associated with object")
            return ret

        fields = super().get_fields()

        # Bypass permission if superuser
        if self.user.is_superuser:
            return fields

        for f in fields:
            if has_right(self.user, self.Meta.model.__name__.lower(), f, "read"):
                ret[f] = fields[f]

        return ret

This seems to work fine, and fields of the child are discarded in the serializer when i either revoke read-rights on Child.c_name or on Parent.child

Solution 2:[2]

If you can not change the nature of you child serializer, as in @Kirill Cherepanov and @Robin van Leeuwen answers, a light but not full-integrated solution would be to manually pass the context in __init__() function :

class ChildSerializer(CustomModelSerializer):
    class Meta:
        fields = ('c_name', )
        model = Child


class ParentSerializer(CustomModelSerializer):

    child = ChildSerializer(many=True, read_only=True)

    class Meta:
        model = Parent
        fields = ('p_name', 'child')

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # We pass the "upper serializer" context to the "nested one"
        self.fields['child'].context.update(self.context)

Solution 3:[3]

You can use serialziers.ListField instead. ListField automatically passes context to it's child. So, here's your code

class ChildSerializer(ModelSerializer):
    class Meta:
        fields = ('c_name', )
        model = Child


class ParentSerializer(ModelSerializer):
    child = serializers.ListField(read_only=True, child=ChildSerializer())

    class Meta:
        model = Parent
        fields = ('p_name', 'child')

Solution 4:[4]

Ok, I have found an ultimate solution that will do exactly what was asked - pass context down to nested serializers. To achieve that one need to override to_representation(self, instance) of the nested serializer, so it looks like:

def to_representation(self, instance):
    # here we update current serializer's context (access it as self._context)
    # to access parent's context we use parent.context
    # if there is no parent than it's the first serializer in the chain and it doesn't need any context except for itself's
    # for example (after all the checks)
    self._context["request"] = self.parent.context["request"]
    # and that is it! The modified context will be used for serialization as if it was passed as usually
    return super().to_representation(instance)

Solution 5:[5]

I know this is an old question, but I had the same question in 2019. Here is my solution:

class MyBaseSerializer(serializers.HyperlinkedModelSerializer):

    def get_fields(self):
        '''
        Override get_fields() method to pass context to other serializers of this base class.

        If the context contains query param "omit_data" as set to true, omit the "data" field
        '''
        fields = super().get_fields()

        # Cause fields with this same base class to inherit self._context
        for field_name in fields:
            if isinstance(fields[field_name], serializers.ListSerializer):
                if isinstance(fields[field_name].child, MyBaseSerializer):
                    fields[field_name].child._context = self._context

            elif isinstance(fields[field_name], MyBaseSerializer):
                fields[field_name]._context = self._context

        # Check for "omit_data" in the query params and remove data field if true
        if 'request' in self._context:
            omit_data = self._context['request'].query_params.get('omit_data', False)

            if omit_data and omit_data.lower() in ['true', '1']:
                fields.pop('data')

        return fields

In the above, I create a serializer base class that overrides get_fields() and passes self._context to any child serializer that has the same base class. For ListSerializers, I attach the context to the child of it.

Then, I check for a query param "omit_data" and remove the "data" field if it's requested.

I hope this is helpful for anybody still looking for answers for this.

Solution 6:[6]

If you are trying to limit the queryset of the child serializer field, then go ahead and use

self.parent.context

inside the child serializer to access the parent context.

like so:

def get_fields(self):
    fields = super().get_fields()
    fields['product'].queryset = Product.objects.filter(company=self.parent.context['company'])
    return fields

This answer led me to find this via debugging and looking at the available variables in child's get_fields function.

Solution 7:[7]

I am using djangorestframework 3.12.xx and the context is automatically propagated to nested serializers.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Robin van Leeuwen
Solution 2
Solution 3 Kirill Cherepanov
Solution 4 Vladimir Vislovich
Solution 5
Solution 6
Solution 7 Devesh Pradhan