122

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.

13 Answers 13

88

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.

Sign up to request clarification or add additional context in comments.

11 Comments

Ok, this works for depth=1. What if I have more levels in the object tree - category has subcategory which has subcategory? I want to represent the whole tree of arbitrary depth with inline objects. Using your approach, I can't define subcategory field in SubCategorySerializer.
Edited with more information on self-referential serializers.
For anyone new viewing this question, I found that for each extra recursive level, I had to a repeat of the last line in the second edit. Strange workaround, but seems to work.
@TomChristie You still get the child repeated at the root tho? How can I stop this?
I'd just like to point out, "base_fields" no longer works. With DRF 3.1.0 "_declared_fields" is where the magic is.
|
77

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

7 Comments

Why is this not the accepted answer? Works perfectly.
This works very simply, I had a much easier time getting this working than the other solutions posted.
This solution doesn't need extra classes and is easier to understand than the parent.parent.__class__ stuff. I like it the most.
This might not be an option if you wanna use the OPTIONS endpoint of your views, it gets stuck in some infinite loop if I used this approach. RecursiveField solution worked for me and re-usable as well.
This method results in a RecursionError when generating an OpenAPI schema in DRF 3.12. The RecursiveField approach is obtuse but avoids this issue.
|
75

@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.

10 Comments

What does the line serializer = self.parent.parent.__class__(value, context=self.context) do. Is it to_representation() method?
This line is the most important part -- it allows the representation of the field to reference the correct serializer. In this example, I believe it would be the CommentSerializer.
I'm sorry. I couldn't understand what this code is doing. I ran it and it works. But I have no idea how it actually works.
This throws an error for me: self.parent.parent is None. There is a simpler solution below using get_fields that seems to do the trick
This is lovely and it works fine: thank you. In case anyone else here is wondering, the import statement you need is from rest_framework_recursive.fields import RecursiveField .
|
32

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/

6 Comments

Works with serializing a MPTTModel. Nice!
You still get the child repeated at the root tho? How can I stop this?
Sorry @Sputnik I don't understand what you mean. What I have given here works for the case where you have an class Blah and it has a field called child_blahs which consists of a list of Blah objects.
This was working great until I upgraded to DRF 3.0, so I posted a 3.0 variation.
@Falcon1 You can filter queryset and only pass root nodes in views like queryset=Class.objects.filter(level=0). It handles rest of the things itself.
|
26

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

1 Comment

For me it came down to a choice between this solution and yprez's solution. They are both clearer and simpler than the solutions posted earlier. The solution here won out because I found that it is the best way to solve the problem presented by the OP here and at the same time support this solution for dynamically selecting fields to be serialized. Yprez's solution causes an infinite recursion or requires additional complications to avoid the recursion and properly select fields.
11

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)

2 Comments

This is great, I had an arbitrarily deep tree that I needed to serialize and this worked like a charm!
Good and very useful answer. When getting children on ModelSerializer you can't specify a queryset for getting child elements. In this case you can do that.
9

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...

1 Comment

As of 2.3.8, there is no convert_object method. But the same thing can be done by overriding to_native method.
9

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.

1 Comment

The queryset with the filter caused an error for me. But this helped to get rid of the repeated field. Override to_representation method in the serializer class: stackoverflow.com/questions/37985581/…
6

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.

2 Comments

Amazing, this worked for me. However, I think the if 'branches' should be changed to if 'subcategories'
Thanks, works for me without passing object here CategorySerializer(obj, many=True).
6

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'])

1 Comment

This is a very thorough solution, however, it's worth noting that your else clause makes certain assumptions about the view. I had to replace mine with return value.pk so it returned primary keys instead of trying to reverse look up the view.
4

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')

Comments

0
class CategoryListSerializer(ModelSerializer):
sub_category = serializers.SerializerMethodField("get_sub_category")

def get_sub_category(self, obj):
    if obj.sub_category:
        serializer = self.__class__(obj.sub_category)
        return serializer.data
    else:
        return None

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

1 Comment

Your answer could be improved with additional supporting information. Please edit to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers in the help center.
0

Share my solution with Django and the library mptt-django.

#View

class categoryView(APIvView):
  serializer_class = CategorySerializer
  def get(self, request, *args, **kwargs):
     categories = Category.objects.filter(parent__isnull=True)
     serializer = self.serializer_class(categories, many=True)
     return Response(serializer.data)

#Serializer

class CategorySerializer(serializers.ModelSerializer):
    subcategories = serializers.SerializerMethodField(
        read_only=True, method_name="get_child_categories")

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

    def get_child_categories(self, obj):
        serializer = CategorySerializer(
            instance=obj.get_children(),
            many=True
        )
        return serializer.data

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.