Reputation: 2711
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?
Upvotes: 61
Views: 23327
Reputation: 613
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()
Upvotes: 0
Reputation: 198
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
Upvotes: 1
Reputation: 803
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
.
Upvotes: 0
Reputation: 1328
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.
Upvotes: 5
Reputation: 199
I am using djangorestframework
3.12.xx and the context is automatically propagated to nested serializers.
Upvotes: 7
Reputation: 391
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)
Upvotes: 13
Reputation: 1470
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)
Upvotes: 37
Reputation: 17877
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.
Upvotes: 3
Reputation: 975
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')
Upvotes: 9
Reputation: 2711
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
Upvotes: 49