Nested annotation fields in Django REST Framework serializer
I'm trying to view nested annotated (aggregated/calculated) fields in a Django REST Framework serializer. This allows for cleaner handling of annotated fields. This post is similar to Aggregate (and other annotated) fields in Django Rest Framework serializers , but I would like to use a similar technique for nesting. Below that method you can see how no nesting works and how nesting doesn't seem to work.
I know this can be achieved manually (using Django View) or using methods that overload the database I'm not interested in. However, maybe there is a performant solution to this problem.
The following works (not nested)
role model
class IceCreamCompany(models.Model):
name = models.CharField(max_length=255)
class IceCreamTruck(models.Model):
company = models.ForeignKey('IceCreamCompany', related_name='trucks')
capacity = models.IntegerField()
class IceCreamTruckDriver(models.Model):
name = models.CharField(max_length=255)
first_name = models.CharField(max_length=255)
truck = models.ForeignKey('IceCreamTruck', related_name='drivers')
serializer
class IceCreamTruckDriverSerializer(serializers.ModelSerializer):
class Meta:
model = IceCreamTruckDriver
fields = ('name', 'first_name')
class IceCreamTruckSerializer(serializers.ModelSerializer):
drivers = IceCreamTruckDriverSerializer(many=True, read_only=True)
class Meta:
model = IceCreamTruck
fields = ('capacity', 'drivers')
class IceCreamCompanySerializer(serializers.ModelSerializer):
trucks = IceCreamTruckSerializer(many=True, read_only=True)
amount_of_trucks = serializers.IntegerField()
class Meta:
model = IceCreamCompany
fields = ('name', 'trucks', 'amount_of_trucks')
Viewset
class IceCreamCompanyViewSet(viewsets.ModelViewSet):
queryset = IceCreamCompany.objects.prefetch_related('trucks', 'trucks__drivers')\
.annotate(amount_of_trucks=Count('trucks'))\
.all()
serializer_class = IceCreamCompanySerializer
result
"results": [
{
"name": "Pete Ice Cream",
"trucks": [
{
"capacity": 35,
"drivers": [
{
"name": "Damian",
"first_name": "Ashley"
},
{
"name": "Wilfrid",
"first_name": "Lesley"
}
]
},
{
"capacity": 30,
"drivers": [
{
"name": "Stevens",
"first_name": "Joseph"
}
]
},
{
"capacity": 30,
"drivers": []
}
],
"amount_of_trucks": 3
}
]
The following is invalid (nested)
same model
serializer
class IceCreamTruckDriverSerializer(serializers.ModelSerializer):
class Meta:
model = IceCreamTruckDriver
fields = ('name', 'first_name')
class IceCreamTruckSerializer(serializers.ModelSerializer):
drivers = IceCreamTruckDriverSerializer(many=True, read_only=True)
amount_of_drivers = serializers.IntegerField()
class Meta:
model = IceCreamTruck
fields = ('capacity', 'drivers', 'amount_of_drivers')
class IceCreamCompanySerializer(serializers.ModelSerializer):
trucks = IceCreamTruckSerializer(many=True, read_only=True)
class Meta:
model = IceCreamCompany
fields = ('name', 'trucks')
Viewset
class IceCreamCompanyViewSet(viewsets.ModelViewSet):
queryset = IceCreamCompany.objects.prefetch_related('trucks', 'trucks__drivers')\
.annotate(trucks__amount_of_drivers=Count('trucks__drivers'))\
.all()
serializer_class = IceCreamCompanySerializer
result
AttributeError at /ice/
Got AttributeError when attempting to get a value for field `amount_of_drivers` on serializer `IceCreamTruckSerializer`.
The serializer field might be named incorrectly and not match any attribute or key on the `IceCreamTruck` instance.
Original exception text was: 'IceCreamTruck' object has no attribute 'amount_of_drivers'.
I got the answer using the Django REST Google group with read_only=True inside the IntegerField, which helped to get rid of the error, but this field doesn't show up anymore. Maybe my annotation is wrong. Anyway, I ended up using custom views in Django as I ended up needing more data. However, you can get data in other ways:
A very elegant solution is to remove the annotation function and use SerializerMethodField which gives me the result.
But: this does raise a lot of questions!
same model
serializer
class IceCreamTruckDriverSerializer(serializers.ModelSerializer):
class Meta:
model = IceCreamTruckDriver
fields = ('name', 'first_name')
class IceCreamTruckSerializer(serializers.ModelSerializer):
drivers = IceCreamTruckDriverSerializer(many=True, read_only=True)
amount_of_drivers = serializers.SerializerMethodField()
def get_amount_of_drivers(self, obj):
return obj.drivers.count()
class Meta:
model = IceCreamTruck
fields = ('capacity', 'drivers', 'amount_of_drivers')
class IceCreamCompanySerializer(serializers.ModelSerializer):
trucks = IceCreamTruckSerializer(many=True, read_only=True)
class Meta:
model = IceCreamCompany
fields = ('name', 'trucks')
Viewset
class IceCreamCompanyViewSet(viewsets.ModelViewSet):
queryset = IceCreamCompany.objects.prefetch_related('trucks', 'trucks__drivers').all()
serializer_class = IceCreamCompanySerializer
result
"results": [
{
"name": "Pete Ice Cream",
"trucks": [
{
"capacity": 35,
"drivers": [
{
"name": "Damian",
"first_name": "Ashley"
},
{
"name": "Wilfrid",
"first_name": "Lesley"
}
],
"amount_of_drivers": 2
},
{
"capacity": 30,
"drivers": [
{
"name": "Stevens",
"first_name": "Joseph"
}
],
"amount_of_drivers": 1
},
{
"capacity": 30,
"drivers": [],
"amount_of_drivers": 0
}
]
}
]
It's also possible to use the following functionality inside the model: Django Rest Framework Ordering on SerializerMethodField (visible in the code itself), but I didn't select it so I didn't have to modify the model too much. This also generates too many queries.