Django rest framework nested self-referential objects

DjangoDjango Rest-Framework

Django Problem Overview


I have model that looks like this:

class Category(models.Model):
    parentCategory = models.ForeignKey('self', blank=True, null=True, related_name='subcategories')
    name = models.CharField(max_length=200)
    description = models.CharField(max_length=500)

I managed to get flat json representation of all categories with serializer:

class CategorySerializer(serializers.HyperlinkedModelSerializer):
    parentCategory = serializers.PrimaryKeyRelatedField()
    subcategories = serializers.ManyRelatedField()

    class Meta:
        model = Category
        fields = ('parentCategory', 'name', 'description', 'subcategories')

Now what I want to do is for subcategories list to have inline json representation of subcategories instead of their ids. How would I do that with django-rest-framework? I tried to find it in documentation, but it seems incomplete.

Django Solutions


Solution 1 - Django

Instead of using ManyRelatedField, use a nested serializer as your field:

class SubCategorySerializer(serializers.ModelSerializer):
    class Meta:
        model = Category
        fields = ('name', 'description')

class CategorySerializer(serializers.ModelSerializer):
    parentCategory = serializers.PrimaryKeyRelatedField()
    subcategories = serializers.SubCategorySerializer()

    class Meta:
        model = Category
        fields = ('parentCategory', 'name', 'description', 'subcategories')

If you want to deal with arbitrarily nested fields you should take a look at the customising the default fields part of the docs. You can't currently directly declare a serializer as a field on itself, but you can use these methods to override what fields are used by default.

class CategorySerializer(serializers.ModelSerializer):
    parentCategory = serializers.PrimaryKeyRelatedField()

    class Meta:
        model = Category
        fields = ('parentCategory', 'name', 'description', 'subcategories')

        def get_related_field(self, model_field):
            # Handles initializing the `subcategories` field
            return CategorySerializer()

Actually, as you've noted the above isn't quite right. This is a bit of a hack, but you might try adding the field in after the serializer is already declared.

class CategorySerializer(serializers.ModelSerializer):
    parentCategory = serializers.PrimaryKeyRelatedField()

    class Meta:
        model = Category
        fields = ('parentCategory', 'name', 'description', 'subcategories')

CategorySerializer.base_fields['subcategories'] = CategorySerializer()

A mechanism of declaring recursive relationships is something that needs to be added.


Edit: Note that there is now a third-party package available that specifically deals with this kind of use-case. See djangorestframework-recursive.

Solution 2 - Django

@wjin's solution was working great for me until I upgraded to Django REST framework 3.0.0, which deprecates to_native. Here's my DRF 3.0 solution, which is a slight modification.

Say you have a model with a self-referential field, for example threaded comments in a property called "replies". You have a tree representation of this comment thread, and you want to serialize the tree

First, define your reusable RecursiveField class

class RecursiveField(serializers.Serializer):
    def to_representation(self, value):
        serializer = self.parent.parent.__class__(value, context=self.context)
        return serializer.data

Then, for your serializer, use the the RecursiveField to serialize the value of "replies"

class CommentSerializer(serializers.Serializer):
    replies = RecursiveField(many=True)

    class Meta:
        model = Comment
        fields = ('replies, ....)

Easy peasy, and you only need 4 lines of code for a re-usable solution.

NOTE: If your data structure is more complicated than a tree, like say a directed acyclic graph (FANCY!) then you could try @wjin's package -- see his solution. But I haven't had any problems with this solution for MPTTModel based trees.

Solution 3 - Django

Another option that works with Django REST Framework 3.3.2:

class CategorySerializer(serializers.ModelSerializer):
    class Meta:
        model = Category
        fields = ('id', 'name', 'parentid', 'subcategories')

    def get_fields(self):
        fields = super(CategorySerializer, self).get_fields()
        fields['subcategories'] = CategorySerializer(many=True)
        return fields

Solution 4 - Django

Late to the game here, but here's my solution. Let's say I'm serializing a Blah, with multiple children also of type Blah.

    class RecursiveField(serializers.Serializer):
        def to_native(self, value):
            return self.parent.to_native(value)

Using this field I can serialize my recursively-defined objects that have many child-objects

    class BlahSerializer(serializers.Serializer):
        name = serializers.Field()
        child_blahs = RecursiveField(many=True)

I wrote a recursive field for DRF3.0 and packaged it for pip https://pypi.python.org/pypi/djangorestframework-recursive/

Solution 5 - Django

I was able to achieve this result using a serializers.SerializerMethodField. I'm not sure if this is the best way, but worked for me:

class CategorySerializer(serializers.ModelSerializer):

    subcategories = serializers.SerializerMethodField(
        read_only=True, method_name="get_child_categories")

    class Meta:
        model = Category
        fields = [
            'name',
            'category_id',
            'subcategories',
        ]

    def get_child_categories(self, obj):
        """ self referral field """
        serializer = CategorySerializer(
            instance=obj.subcategories_set.all(),
            many=True
        )
        return serializer.data

Solution 6 - Django

Another option would be to recurse in the view that serializes your model. Here's an example:

class DepartmentSerializer(ModelSerializer):
    class Meta:
        model = models.Department


class DepartmentViewSet(ModelViewSet):
    model = models.Department
    serializer_class = DepartmentSerializer

    def serialize_tree(self, queryset):
        for obj in queryset:
            data = self.get_serializer(obj).data
            data['children'] = self.serialize_tree(obj.children.all())
            yield data

    def list(self, request):
        queryset = self.get_queryset().filter(level=0)
        data = self.serialize_tree(queryset)
        return Response(data)

    def retrieve(self, request, pk=None):
        self.object = self.get_object()
        data = self.serialize_tree([self.object])
        return Response(data)

Solution 7 - Django

I recently had the same problem and came up with a solution that seems to work so far, even for arbitrary depth. The solution is a small modification of the one from Tom Christie:

class CategorySerializer(serializers.ModelSerializer):
    parentCategory = serializers.PrimaryKeyRelatedField()

    def convert_object(self, obj):
        #Add any self-referencing fields here (if not already done)
        if not self.fields.has_key('subcategories'):
            self.fields['subcategories'] = CategorySerializer()      
        return super(CategorySerializer,self).convert_object(obj) 

    class Meta:
        model = Category
        #do NOT include self-referencing fields here
        #fields = ('parentCategory', 'name', 'description', 'subcategories')
        fields = ('parentCategory', 'name', 'description')
#This is not needed
#CategorySerializer.base_fields['subcategories'] = CategorySerializer()

I'm not sure it can reliably work in any situation, though...

Solution 8 - Django

This is an adaptation from the caipirginka solution that works on drf 3.0.5 and django 2.7.4:

class CategorySerializer(serializers.ModelSerializer):

    def to_representation(self, obj):
        #Add any self-referencing fields here (if not already done)
        if 'branches' not in self.fields:
            self.fields['subcategories'] = CategorySerializer(obj, many=True)      
        return super(CategorySerializer, self).to_representation(obj) 
    
    class Meta:
        model = Category
        fields = ('id', 'description', 'parentCategory')

Note that the CategorySerializer in 6th line is called with the object and the many=True attribute.

Solution 9 - Django

I thought I'd join in on the fun!

Via wjin and Mark Chackerian I created a more general solution, which works for direct tree-like models and tree structures which have a through model. I'm not sure if this belongs in it's own answer but I thought I might as well put it somewhere. I included a max_depth option which will prevent infinite recursion, at the deepest level children are represented as URLS (that's the final else clause if you'd rather it wasn't a url).

from rest_framework.reverse import reverse
from rest_framework import serializers

class RecursiveField(serializers.Serializer):
    """
    Can be used as a field within another serializer,
    to produce nested-recursive relationships. Works with
    through models, and limited and/or arbitrarily deep trees.
    """
    def __init__(self, **kwargs):
        self._recurse_through = kwargs.pop('through_serializer', None)
        self._recurse_max = kwargs.pop('max_depth', None)
        self._recurse_view = kwargs.pop('reverse_name', None)
        self._recurse_attr = kwargs.pop('reverse_attr', None)
        self._recurse_many = kwargs.pop('many', False)

        super(RecursiveField, self).__init__(**kwargs)

    def to_representation(self, value):
        parent = self.parent
        if isinstance(parent, serializers.ListSerializer):
            parent = parent.parent

        lvl = getattr(parent, '_recurse_lvl', 1)
        max_lvl = self._recurse_max or getattr(parent, '_recurse_max', None)

        # Defined within RecursiveField(through_serializer=A)
        serializer_class = self._recurse_through
        is_through = has_through = True

        # Informed by previous serializer (for through m2m)
        if not serializer_class:
            is_through = False
            serializer_class = getattr(parent, '_recurse_next', None)

        # Introspected for cases without through models.
        if not serializer_class:
            has_through = False
            serializer_class = parent.__class__

        if is_through or not max_lvl or lvl <= max_lvl: 
            serializer = serializer_class(
                value, many=self._recurse_many, context=self.context)

            # Propagate hereditary attributes.
            serializer._recurse_lvl = lvl + is_through or not has_through
            serializer._recurse_max = max_lvl
            
            if is_through:
                # Delay using parent serializer till next lvl.
                serializer._recurse_next = parent.__class__

            return serializer.data
        else:
            view = self._recurse_view or self.context['request'].resolver_match.url_name
            attr = self._recurse_attr or 'id'
            return reverse(view, args=[getattr(value, attr)],
                           request=self.context['request'])

Solution 10 - Django

This solution is almost similar with the other solutions posted here but has a slight difference in terms of child repetition problem at the root level( if you think its as a problem). For an example

class RecursiveSerializer(serializers.Serializer):
    def to_representation(self, value):
        serializer = self.parent.parent.__class__(value, context=self.context)
        return serializer.data

class CategoryListSerializer(ModelSerializer):
    sub_category = RecursiveSerializer(many=True, read_only=True)

    class Meta:
        model = Category
        fields = (
            'name',
            'slug',
            'parent', 
            'sub_category'
    )

and if you have this view

class CategoryListAPIView(ListAPIView):
    queryset = Category.objects.all()
    serializer_class = CategoryListSerializer

This will produce the following result,

[
{
    "name": "parent category",
    "slug": "parent-category",
    "parent": null,
    "sub_category": [
        {
            "name": "child category",
            "slug": "child-category",
            "parent": 20,  
            "sub_category": []
        }
    ]
},
{
    "name": "child category",
    "slug": "child-category",
    "parent": 20,
    "sub_category": []
}
]

Here the parent category has a child category and the json representation is exactly what we want it to be represent.

but you can see there is a repetition of the child category at the root level.

As some people are asking in the comment sections of the above posted answers that how can we stop this child repetition at the root level, just filter your queryset with parent=None, like as the following

class CategoryListAPIView(ListAPIView):
    queryset = Category.objects.filter(parent=None)
    serializer_class = CategoryListSerializer

it will solve the problem.

NOTE: This answer might not directly related with the question, but the problem is somehow related. Also this approach of using RecursiveSerializer is expensive. Better if you use other options which is performance prone.

Solution 11 - Django

With Django REST framework 3.3.1, I needed the following code to get subcategories added to categories:

models.py

class Category(models.Model):

	id = models.AutoField(
		primary_key=True
	)

	name = models.CharField(
		max_length=45, 
		blank=False, 
		null=False
	)

	parentid = models.ForeignKey(
		'self',
		related_name='subcategories',
		blank=True,
		null=True
    )

	class Meta:
		db_table = 'Categories'

serializers.py

class SubcategorySerializer(serializers.ModelSerializer):

    class Meta:
        model = Category
        fields = ('id', 'name', 'parentid')


class CategorySerializer(serializers.ModelSerializer):
    subcategories = SubcategorySerializer(many=True, read_only=True)

    class Meta:
        model = Category
        fields = ('id', 'name', 'parentid', 'subcategories')

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
QuestionJacek ChmielewskiView Question on Stackoverflow
Solution 1 - DjangoTom ChristieView Answer on Stackoverflow
Solution 2 - DjangoMark ChackerianView Answer on Stackoverflow
Solution 3 - DjangoyprezView Answer on Stackoverflow
Solution 4 - DjangowjinView Answer on Stackoverflow
Solution 5 - DjangojarussiView Answer on Stackoverflow
Solution 6 - DjangoStefan ReinhardView Answer on Stackoverflow
Solution 7 - DjangocaipirginkaView Answer on Stackoverflow
Solution 8 - DjangoWicho ValdeavellanoView Answer on Stackoverflow
Solution 9 - DjangoWill SView Answer on Stackoverflow
Solution 10 - DjangoMd. Tanvir RaihanView Answer on Stackoverflow
Solution 11 - DjangoAndraDView Answer on Stackoverflow