118

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.

Matt Swain
  • 3,827
  • 4
  • 25
  • 36
Jacek Chmielewski
  • 1,763
  • 4
  • 16
  • 18

12 Answers12

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.

Tom Christie
  • 33,394
  • 7
  • 101
  • 86
  • 5
    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. – Jacek Chmielewski Nov 14 '12 at 11:04
  • Edited with more information on self-referential serializers. – Tom Christie Nov 14 '12 at 11:45
  • Now i got `KeyError at /api/category/ 'subcategories'`. Btw thanks for your super-fast replies :) – Jacek Chmielewski Nov 14 '12 at 14:42
  • The workaround seems to work only for depth=1, then subcategories are listed as primary keys again. Anyway I decided to go with flat representation in my api and let client traverse the tree himself. Thank you for helping :). – Jacek Chmielewski Nov 15 '12 at 14:57
  • 4
    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. – Jeremy Blalock Apr 11 '13 at 00:55
  • This doesn't work for arbitrary depth since the field is being initialized with the version of serializer that still does not have the recursive field on it. By repeating the last line, we can push it down as many levels of depth as required. But I think the solution by @caipirginka is better. – abhaga Oct 26 '13 at 09:11
  • Hi @TomChristie can you please help me in this question? http://stackoverflow.com/questions/23313203/django-rest-framework-serializer-not-giving-the-expected-output?noredirect=1#comment35692571_23313203 – Abhishek Apr 26 '14 at 16:45
  • 1
    @TomChristie You still get the child repeated at the root tho? How can I stop this? – Prometheus Oct 02 '14 at 11:37
  • 23
    I'd just like to point out, "base_fields" no longer works. With DRF 3.1.0 "_declared_fields" is where the magic is. – Travis Swientek Mar 11 '15 at 01:34
  • djangorestframework-recursive works but the count output of my serializer is wrong. It only counts root categories, not it's children. Any ideas? It's the same with the solutions below. – Lucas Veiga May 03 '17 at 01:57
  • Doesn't this cause RBAR queries against the table for every row that's returned? – JTW Nov 29 '18 at 02:07
74

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

Mark Chackerian
  • 21,866
  • 6
  • 108
  • 99
  • 1
    What does the line serializer = self.parent.parent.__class__(value, context=self.context) do. Is it to_representation() method? – Mauricio May 07 '16 at 07:16
  • 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. – Mark Chackerian May 08 '16 at 01:36
  • 1
    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. – Mauricio May 10 '16 at 21:52
  • Try putting in some print statements like `print self.parent.parent.__class__` and `print self.parent.parent` – Mark Chackerian May 11 '16 at 04:37
  • Solution works but the count output of my serializer is wrong. It only counts root nodes. Any ideas? It's the same with djangorestframework-recursive. – Lucas Veiga May 03 '17 at 01:29
  • Sounds like you may need to override a method to account for your structure, and the actual implementation may depend on exactly which kinds of nodes you want to count. – Mark Chackerian May 03 '17 at 14:20
  • 1
    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 – Nick BL Nov 20 '19 at 18:34
  • 1
    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` . – Marnanel Thurman Apr 15 '20 at 16:20
  • I am alwyas getting object is not iterabale error ;( – lord stock Jul 26 '22 at 13:40
  • @lordstock: the field likely only works with `multiple=True` and thus for a *collection* so a many-to-many field or a one-to-many (reversed foreign key). While it fixes the problem to some extent, it only works in some specific cases. – Willem Van Onsem May 28 '23 at 11:21
74

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
yprez
  • 14,854
  • 11
  • 55
  • 70
  • 8
    Why is this not the accepted answer? Works perfectly. – Karthik RP Sep 03 '18 at 11:26
  • 7
    This works very simply, I had a much easier time getting this working than the other solutions posted. – Nick BL Nov 20 '19 at 18:32
  • This solution doesn't need extra classes and is easier to understand than the `parent.parent.__class__` stuff. I like it the most. – SergiyKolesnikov Oct 30 '20 at 14:10
  • In python 3, it can be like this: `fields = super().get_fields()` – Elinaldo Monteiro Dec 02 '20 at 15:58
  • 3
    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. – Prasad Pilla Jan 08 '21 at 18:57
  • 4
    This method results in a `RecursionError` when generating an OpenAPI schema in DRF 3.12. The `RecursiveField` approach is obtuse but avoids this issue. – w_hile May 15 '21 at 21:09
  • I'm also seeing a `RecursionError` in some cases (i.e. when django tries to display a stacktrace for some other issue). Appears to be causing infinite recursion in: `site-packages/rest_framework/utils/representation.py", line 81 in serializer_repr` – enerve Jul 14 '21 at 06:03
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/

the Tin Man
  • 158,662
  • 42
  • 215
  • 303
wjin
  • 954
  • 1
  • 8
  • 13
24

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
gitaarik
  • 42,736
  • 12
  • 98
  • 105
jarussi
  • 1,491
  • 1
  • 13
  • 25
  • 1
    For me it came down to a choice between this solution and [yprez's solution](http://stackoverflow.com/a/35897731/1906307). 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](http://stackoverflow.com/a/31979444/1906307). Yprez's solution causes an infinite recursion *or* requires additional complications to avoid the recursion and properly select fields. – Louis Jul 24 '16 at 18:53
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)
Stefan Reinhard
  • 261
  • 3
  • 6
  • This is great, I had an arbitrarily deep tree that I needed to serialize and this worked like a charm! – Víðir Orri Reynisson Jun 10 '14 at 12:56
  • 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. – Efrin Jun 25 '14 at 08:08
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...

caipirginka
  • 266
  • 3
  • 4
  • 1
    As of 2.3.8, there is no convert_object method. But the same thing can be done by overriding to_native method. – abhaga Oct 26 '13 at 09:05
7

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.

Md. Tanvir Raihan
  • 4,075
  • 9
  • 37
  • 70
  • 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: https://stackoverflow.com/questions/37985581/how-to-dynamically-remove-fields-from-serializer-output#answer-37997364 – Aaron Aug 25 '20 at 07:53
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.

  • Amazing, this worked for me. However, I think the `if 'branches'` should be changed to `if 'subcategories'` – vabada Sep 29 '16 at 13:04
  • Thanks, works for me without passing object here `CategorySerializer(obj, many=True)`. – Ledorub Oct 28 '21 at 15:02
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'])
Community
  • 1
  • 1
Will S
  • 744
  • 8
  • 17
  • 1
    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. – Soviut May 25 '16 at 21:10
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')
AndraD
  • 2,830
  • 6
  • 38
  • 48
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
    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](/help/how-to-answer). – Community May 12 '23 at 15:31