In django, how to serialize mptt tree?

3.5k views Asked by At

The follows is my code:

class File(MPTTModel):
    name=models.CharField(max_length=36, primary_key=True)
    parent = TreeForeignKey('self', null=True, blank=True, related_name='children', db_index=True)
    num=models.IntegerField(null=True)
    class MPTTMeta:
        order_insertion_by = ['name']

And I try to serialize this class using the following code:

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

class FileSerializer(serializers.ModelSerializer):
    parent=RecursiveField(many=True)
    class Meta:
        model = File
        fields=('name','num','parent')

But I fail in that I can only output the contents of the root node of this tree.It seems the serializer cannot access the children of the root, and further the children of the children... The specific problem is that in the output, the 'parent' is shown to be 'null', but in fact it has 4 children and each of them contains several descendants. What's wrong with my code? Thank you all for helping me!

2

There are 2 answers

1
spectras On BEST ANSWER

There is no magic about neither MPTT nor REST framework.

MPTT adds new fields into your module, so as to implement a nested set model. It also tracks an upwards link from children to their parent, which it uses for some optimizations, and to rebuild the nested set tree, should it get corrupted.

So basically, your model has the following fields name/num that you added manually, parent which you added to trigger MPTT, and the following automatic fields:

  • tree_id: a tree identifier. All nodes attached to the same root share the same tree_id.
  • ̀level: the depth of the node within the tree.
  • lft/rght: the nested set indices. See the link above, but the basic idea is a node is a descendant of another node if its lft is greated or equal and its rght is lower or equal to that of the other node.

REST framework is not mptt-aware, and does not need to be. It will just see a regular model with 7 fields, which it will happily serialize.

While you may implement a recursive serializer to shape the serialized representation in a nested object of objects of objects of objects of..., it is usually not a good idea at this point.

Now, if you actually want to do that, you need to do it the other way. You must serialize the root nodes, and make sure their serialized representation recursively includes all of their children. Not the other way around, like you tried here.

The idea would be to construct something like this:

class FileSerializer(serializers.ModelSerializer):
    children = FileSerializer(many=True)
    class Meta:
        model = File
        fields=('name','num')

But you cannot do it this way, as FileSerializer is not defined at the point you want it. You could try to override the constructor and insert the additional serializer there, like this:

class FileSerializer(serializers.ModelSerializer):
    class Meta:
        model = File
        fields=('name','num')

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

Untested, but you get the idea.

However:

  • Doing it in this naive way is a really bad idea, as every non-leaf node in your tree will trigger an additional query to the database to fetch its children.
  • This will not work for deserialization.

How about you just serialize flat nodes, and rebuild the object tree on the client side if you really need to?

[{'id': 1, 'name': 'foo', 'parent': null},  // /foo
 {'id': 2, 'name': 'bar1', 'parent': 1},    // /foo/bar1
 {'id': 3, 'name': 'bar2', 'parent': 1},    // /foo/bar2
 {'id': 4, 'name': 'foo2', 'parent': null}, // /foo2
 {'id': 5, 'name': 'baz1', 'parent': 4},    // /foo2/baz1
 {'id': 6, 'name': 'baz2', 'parent': 4}]    // /foo2/baz2
0
Elias Prado On

Update 2021.

For those who needs to create a nested tree I upgraded the @spectras answer and I managed to return a nested tree. You only need to add 'required=False' in the FileSerializer on get_fields() and in your viewsets you have to find a way to return "only" the first node.

class FileSerializer(serializers.ModelSerializer):
    class Meta:
        model = File
        fields=('name','num')

    def get_fields(self):
        fields = super(FileSerializer, self).get_fields()
        fields['children'] = FileSerializer(many=True, required=False)
        return fields

In my case, on viewsets, I filtered to return only the items that has a parent=0.

To continue the example above:

class FileViewSet(viewsets.ModelViewSet):
    """
    Viewset to be used on Urls.py
    """
    serializer_class = File

    def get_queryset(self):
        queryset = File.objects.filter(level=0)
        return queryset

You'll have an endpoint like this:

[
     {
      'id': 1,
      'name': 'foo',
      'parent': 0,
      'children':[
         {
           'id': 2,
           'name': 'bar2',
           'parent': 1,
           'children':[
              {
                'id': 3,
                'name': 'bar3',
                'parent': 2,
                'children':[]
               }
             ]
          } 
       ]
     }, 
     {
      'id': 4,
      'name': 'bar2',
      'parent': 0,
      'children':[]
     }
]

And if you need to UPDATE any node, just add the id of the node as you want to update normally.