Django rest framework nesteds objetos autorreferenciales

Tengo un modelo que se ve así:

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) 

Logré obtener una representación json plana de todas las categorías con el serializador:

 class CategorySerializer(serializers.HyperlinkedModelSerializer): parentCategory = serializers.PrimaryKeyRelatedField() subcategories = serializers.ManyRelatedField() class Meta: model = Category fields = ('parentCategory', 'name', 'description', 'subcategories') 

Ahora lo que quiero hacer es que la lista de subcategorías tenga una representación json en línea de las subcategorías en lugar de sus identificadores. ¿Cómo podría hacer eso con django-rest-framework? Traté de encontrarlo en la documentación, pero parece incompleto.

En lugar de utilizar ManyRelatedField, use un serializador nested como su campo:

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

Si desea tratar con campos nesteds arbitrariamente, debe echar un vistazo a la personalización de la parte de los campos predeterminada de los documentos. Actualmente, no puede declarar directamente un serializador como un campo en sí mismo, pero puede usar estos métodos para anular qué campos se usan por defecto.

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

En realidad, como has notado, lo anterior no está del todo bien. Esto es un truco, pero puede intentar agregar el campo después de que el serializador ya haya sido declarado.

 class CategorySerializer(serializers.ModelSerializer): parentCategory = serializers.PrimaryKeyRelatedField() class Meta: model = Category fields = ('parentCategory', 'name', 'description', 'subcategories') CategorySerializer.base_fields['subcategories'] = CategorySerializer() 

Un mecanismo para declarar relaciones recursivas es algo que debe agregarse.


Editar : tenga en cuenta que ahora hay un paquete de terceros disponible que trata específicamente este tipo de caso de uso. Ver djangorestframework-recursive .

La solución de @wjin me funcionaba muy bien hasta que actualicé a Django REST framework 3.0.0, que desaprueba to_native . Aquí está mi solución DRF 3.0, que es una pequeña modificación.

Supongamos que tiene un modelo con un campo autorreferencial, por ejemplo, comentarios en una propiedad llamada “respuestas”. Tienes una representación en árbol de este hilo de comentarios, y quieres serializar el árbol

Primero, defina su clase RecursiveField reutilizable

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

Luego, para su serializador, use el RecursiveField para serializar el valor de “respuestas”

 class CommentSerializer(serializers.Serializer): replies = RecursiveField(many=True) class Meta: model = Comment fields = ('replies, ....) 

Easy peasy, y solo necesitas 4 líneas de código para una solución reutilizable.

NOTA: Si su estructura de datos es más complicada que un árbol, como por ejemplo, un gráfico acíclico dirigido (¡FANCY!), Entonces podría probar el paquete de @ wjin: ver su solución. Pero no he tenido ningún problema con esta solución para árboles basados ​​en MPTTModel.

Tarde en el juego aquí, pero esta es mi solución. Digamos que estoy serializando un Blah, con varios hijos también de tipo Blah.

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

Usando este campo puedo serializar mis objetos definidos recursivamente que tienen muchos objetos secundarios

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

Escribí un campo recursivo para DRF3.0 y lo empaqué para pip https://pypi.python.org/pypi/djangorestframework-recursive/

Otra opción que funciona con 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 

Recientemente tuve el mismo problema y se me ocurrió una solución que parece funcionar hasta el momento, incluso para profundidad arbitraria. La solución es una pequeña modificación de la de 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() 

Sin embargo, no estoy seguro si puede funcionar de manera confiable en cualquier situación …

Otra opción sería recurrir en la vista que serializa su modelo. Aquí hay un ejemplo:

 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) 

Esta es una adaptación de la solución caipirginka que funciona en drf 3.0.5 y 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') 

Tenga en cuenta que el CategorySerializer en la 6ª línea se llama con el objeto y el atributo many = True.

Pude lograr este resultado utilizando serializers.SerializerMethodField . No estoy seguro de si esta es la mejor manera, pero funcionó para mí:

  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 

¡Pensé en unirme a la diversión!

A través de wjin y Mark Chackerian creé una solución más general, que funciona para modelos directos de árbol y estructuras de árbol que tienen un modelo directo. No estoy seguro de si esto pertenece a su propia respuesta, pero pensé que podría ponerlo en alguna parte. Incluí una opción max_depth que evitará la recursión infinita, en el nivel más profundo los niños se representan como URLS (esa es la última cláusula else si prefieres que no fuera una 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']) 

Con Django REST framework 3.3.1, necesitaba el siguiente código para obtener subcategorías agregadas a categorías:

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