context in nested serializers django rest framework
Asked Answered
D

10

60

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?

Dianoetic answered 31/5, 2015 at 17:33 Comment(0)
D
49

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

Dianoetic answered 1/6, 2015 at 7:53 Comment(2)
This doesn't help if I want the nested serializer to be writeable.Selector
Aye bro this is really late but I love you. Been so annoyed for hours trying to figure this out. Thank you!Cranford
R
31

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)
Roundtree answered 22/10, 2019 at 13:50 Comment(1)
That is what I was looking for. Accepted answer is how it normally works for read serializers. But this is better for writable ones.Ursi
G
11

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)
Gullet answered 20/5, 2020 at 11:41 Comment(1)
I just tested this approach and it worked great for me, and feel like a far simpler setup that the other solution is given. I actually wanted to pull all of the context down, not just a single property. So I ended up making a generic Mixin that I could use in multiple serializer using this approach.Barneybarnhart
R
10

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')
Rachele answered 26/10, 2016 at 10:51 Comment(2)
Thanks for the tip!Sapling
@Kirill, When I tried this, I got: "Child object is not iterable. In my case, my Child is not "many=True". Any thoughts?Teeterboard
H
6

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

Harker answered 30/3, 2022 at 9:41 Comment(4)
Strange, I'm using 3.13.1 and it's not.Drank
I'm using 3.13.1 and the context is automatically propagated to children....Hospitalize
Works on 3.14.0. Thank you!Baryta
Also on 3.14 and context is passed to the children, and nested grandchildren; accessed with request = self.context.get("request"). This should be the preferred approach from 2024.Nonna
H
4

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.

Helban answered 12/7, 2021 at 18:55 Comment(0)
F
3

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.

Fidelia answered 9/8, 2019 at 20:48 Comment(2)
This seems to be a very verbose/complicated solution. What are its advantages over Kirill Cherepanovs solution?Eyrir
I like this answer for it's reuse. In my case, I have a serializer with 36 fields that also go to different serializers. I'm going to extend on this idea and do 2 mixins. One to PassContext, which will do what this solution does, and one to InheritContext, which is the type check to see if the child should actually receive the context.Outworn
E
1

With newest releases of Django REST Framework, the context is passed from parent to child serializer without need to override __init__ method, all you have to do is pass context when you call child serializer:


class ChildSerializer(serializers.ModelSerializer):

   class Meta:

      model = ChildModel
      fields = ['title']

    title = serializers.SerializerMethodField()

    def get_title(self, instance):

       request = self.context.get('request')

       #TODO

       return


class ParentSerializer(serializers.ModelSerializer):

   class Meta:

      model = ParentModel
      fields = ['details']

   details = serializers.SerializerMethodField()

   def get_details(self, instance):

      child_instances = ChildModel.objects.all()

      return ChildSerializer(
            instance=child_instances,
            many=True,
            read_only=True,
            context=self.context
      ).data

Elo answered 21/3, 2023 at 16:35 Comment(0)
E
1

Improved code from "Vladimir Vislovich" answer:

# mixin for child serializers

class NestedSerializerContextMixin:
    def to_representation(self, instance):
        if hasattr(self, 'parent'):
            for name, value in self.parent.context.items():
                if name not in self.context:
                    self.context[name] = value

        return super().to_representation(instance)


# and to use it

class ChildSerializer(NestedSerializerContextMixin, serializers.ModelSerializer):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        
        # self.context have all parent custom attributes in context
        print(self.context)

PS: Don't forget, that if you use in ViewSet this method

serializer = self.get_serializer(data, context=context)

than "context" would not be applied, because in method "self.get_serializer" there is a code:

kwargs['context'] = self.get_serializer_context()
Ecuador answered 2/2 at 16:41 Comment(0)
H
0

I read the source code and I believe that it is not necessary to pass the context to nested field serializers as they have access to it.
Calling for the nested field serializer's context will return the root context. Here is the code and it has not been changed for 9 years.
If you want some explanation, in this example consider field is child. Calling child.context will call the above mentioned property function which within itself uses self.root. root property function will be resolved to ParentSerializer in this case.
So calling child.context will result in calling parent.context.

Hospitalize answered 11/3, 2023 at 0:58 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.