Reputation: 1793
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.
Upvotes: 121
Views: 75152
Reputation: 1
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
Upvotes: 0
Reputation: 1
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'
)
Upvotes: 0
Reputation: 4285
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.
Upvotes: 9
Reputation: 1589
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
Upvotes: 26
Reputation: 33921
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.
Upvotes: 88
Reputation: 964
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/
Upvotes: 32
Reputation: 15154
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
Upvotes: 77
Reputation: 2850
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')
Upvotes: 4
Reputation: 23592
@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.
Upvotes: 75
Reputation: 755
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'])
Upvotes: 6
Reputation: 71
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.
Upvotes: 6
Reputation: 261
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)
Upvotes: 11
Reputation: 266
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...
Upvotes: 9