How do you filter a nested serializer in Django Rest Framework?

DjangoDjango Rest-Framework

Django Problem Overview


In Django Rest Framework, how do you filter a serializer when it's nested in another serializer?

My filters are imposed in the DRF viewsets, but when you call a serializer from inside another serializer, the viewset of the nested serializer never gets called, so the nested results appear unfiltered.

I have tried adding a filter on originating viewset, but it doesn't seem to filter the nested results because the nested results get called as a separate pre-fretched query. (The nested serializer is a reverse lookup, you see.)

Is it possible to add a get_queryset() override in the nested serializer itself (moving it out of the viewset), to add the filter there? I've tried that, too, with no luck.

This is what I tried, but it doesn't even seem to get called:

class QuestionnaireSerializer(serializers.ModelSerializer):
    edition = EditionSerializer(read_only=True)
    company = serializers.StringRelatedField(read_only=True)
    
    class Meta:
        model = Questionnaire
        
    def get_queryset(self):
        query = super(QuestionnaireSerializer, self).get_queryset(instance)
        if not self.request.user.is_staff:
            query = query.filter(user=self.request.user, edition__hide=False)
        return query

Django Solutions


Solution 1 - Django

You can subclass the ListSerializer and overwrite the to_representation method.

By default the to_representation method calls data.all() on the nested queryset. So you effectively need to make data = data.filter(**your_filters) before the method is called. Then you need to add your subclassed ListSerializer as the list_serializer_class on the meta of the nested serializer.

  1. subclass ListSerializer, overwriting to_representation and then calling super
  2. add subclassed ListSerializer as the meta list_serializer_class on the nested Serializer

Here is the relevant code for your sample.

class FilteredListSerializer(serializers.ListSerializer):

	def to_representation(self, data):
		data = data.filter(user=self.context['request'].user, edition__hide=False)
		return super(FilteredListSerializer, self).to_representation(data)


class EditionSerializer(serializers.ModelSerializer):
	
    class Meta:
		list_serializer_class = FilteredListSerializer
        model = Edition


class QuestionnaireSerializer(serializers.ModelSerializer):
    edition = EditionSerializer(read_only=True)
    company = serializers.StringRelatedField(read_only=True)

    class Meta:
        model = Questionnaire

Solution 2 - Django

While all the above answers work, I find the use of Django's Prefetch object the easiest way of all.

Say a Restaurant obj has a lot of MenuItems, some of which are is_removed == True, and you only want those that are not removed.

In RestaurantViewSet, do something like

from django.db.models import Prefetch

queryset = Restaurant.objects.prefetch_related(
    Prefetch('menu_items', queryset=MenuItem.objects.filter(is_removed=False), to_attr='filtered_menu_items')
)

In RestaurantSerializer, do something like

class RestaurantSerializer(serializers.ModelSerializer):
    menu_items = MenuItemSerializer(source='filtered_menu_items', many=True, read_only=True)

Solution 3 - Django

Tested many solutions from SO and other places.

Found only one working solution for Django 2.0 + DRF 3.7.7.

Define a method in model which has nested class. Craft a filter that will fit your needs.

class Channel(models.Model):
    name = models.CharField(max_length=40)
    number = models.IntegerField(unique=True)
    active = models.BooleanField(default=True)

    def current_epg(self):
        return Epg.objects.filter(channel=self, end__gt=datetime.now()).order_by("end")[:6]


class Epg(models.Model):
    start = models.DateTimeField()
    end = models.DateTimeField(db_index=True)
    title = models.CharField(max_length=300)
    description = models.CharField(max_length=800)
    channel = models.ForeignKey(Channel, related_name='onair', on_delete=models.CASCADE)

.

class EpgSerializer(serializers.ModelSerializer):
	class Meta:
		model = Epg
		fields = ('channel', 'start', 'end', 'title', 'description',)


class ChannelSerializer(serializers.ModelSerializer):
	onair = EpgSerializer(many=True, read_only=True, source="current_epg")

	class Meta:
		model = Channel
		fields = ('number', 'name', 'onair',)

Pay attention to source="current_epg" and you'll get the point.

Solution 4 - Django

I find it easier, and more straight forward, to use a SerializerMethodField on the serializer field you want to filter.

So you would do something like this.

class CarTypesSerializer(serializers.ModelSerializer):

    class Meta:
        model = CarType
        fields = '__all__'


class CarSerializer(serializers.ModelSerializer):

    car_types = serializers.SerializerMethodField()

    class Meta:
        model = Car
        fields = '__all__'

    def get_car_types(self, instance):
        # Filter using the Car model instance and the CarType's related_name
        # (which in this case defaults to car_types_set)
        car_types_instances = instance.car_types_set.filter(brand="Toyota")
        return CarTypesSerializer(car_types_instances, many=True).data

This saves you from having to create many overrides of the serializers.ListSerializer if you need different filtering criteria for different serializers.

It also has the extra benefit of seeing exactly what the filter does within the serializer instead of diving into a subclass definition.

Of course the downside is if you have a serializer with many nested objects that all need to be filtered in some way. It could cause the serializer code to greatly increase. It's up to you how you would like to filter.

Hope this helps!

Solution 5 - Django

> When a serializer is instantiated and many=True is passed, a > ListSerializer instance will be created. The serializer class then > becomes a child of the parent ListSerializer > > This method takes the target of the field as the value argument, and > should return the representation that should be used to serialize the > target. The value argument will typically be a model instance. > > Below is the example of the nested serializer

class UserSerializer(serializers.ModelSerializer):
    """ Here many=True is passed, So a ListSerializer instance will be 
     created"""
    system = SystemSerializer(many=True, read_only=True)

    class Meta:
        model = UserProfile
        fields = ('system', 'name')

class FilteredListSerializer(serializers.ListSerializer):
    
    """Serializer to filter the active system, which is a boolen field in 
       System Model. The value argument to to_representation() method is 
      the model instance"""
    
    def to_representation(self, data):
        data = data.filter(system_active=True)
        return super(FilteredListSerializer, self).to_representation(data)

class SystemSerializer(serializers.ModelSerializer):
    mac_id = serializers.CharField(source='id')
    system_name = serializers.CharField(source='name')
    serial_number = serializers.CharField(source='serial')

    class Meta:
        model = System
        list_serializer_class = FilteredListSerializer
        fields = (
            'mac_id', 'serial_number', 'system_name', 'system_active', 
        )

In view:

class SystemView(viewsets.GenericViewSet, viewsets.ViewSet):
    def retrieve(self, request, email=None):
        data = get_object_or_404(UserProfile.objects.all(), email=email)
        serializer = UserSerializer(data)
        return Response(serializer.data)

Solution 6 - Django

The following worked for me, from self.context['view'], You can get the filter params inside the serializer and use it however you want.

class ShipmentDocumentSerializer(serializers.ModelSerializer):

    class Meta:
       model = Document
       fields = ['id', 'created_date', 'consignment', 'document', 'org', 'title' ]

class ShipmentDocumentTypeSerializer(serializers.ModelSerializer):
   documents = serializers.SerializerMethodField()

   class Meta:
       model = DocumentType
       fields = ['id', 'type', 'documents']

    def get_documents(self, instance):
        consignment_id=self.context['view'].kwargs['consignment_id']
        queryset = Document.objects.filter(consignment__id=consignment_id)
        return ShipmentDocumentSerializer(queryset, many=True).data

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionJohnView Question on Stackoverflow
Solution 1 - DjangoinperspectiveView Answer on Stackoverflow
Solution 2 - DjangoDennis LauView Answer on Stackoverflow
Solution 3 - DjangodudditsView Answer on Stackoverflow
Solution 4 - DjangoRob BarberView Answer on Stackoverflow
Solution 5 - DjangoVinay KumarView Answer on Stackoverflow
Solution 6 - Djangoviraj s solankiView Answer on Stackoverflow