Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

django rest framework - how do you flatten nested data?

I have a 'through' model governing a many to many relationship and i want to be able to return the 'through' model and the target model as flat data, as opposed to having the target model nested.

So using the standard example for a many to many with a through, say these are the models,

class Person(models.Model):     first_name = models.CharField(max_length=128)     last_name = models.CharField(max_length=128)     favourite_food = models.CharField(max_length=128)  class Group(models.Model):     name = models.CharField(max_length=128)     members = models.ManyToManyField(Person, through='Membership')  class Membership(models.Model):     person = models.ForeignKey(Person)     group = models.ForeignKey(Group)     date_joined = models.DateField()     invite_reason = models.CharField(max_length=64) 

So the serializers i have at the moment to return Membership items are,

class MembershipSerializer(serializers.HyperlinkedModelSerializer):     person = PersonSerializer()      class Meta:         model = Membership         fields = ('id', 'url', 'group', 'date_joined', 'invite_reason', 'person')  class PersonSerializer(serializers.HyperlinkedModelSerializer):     class Meta:         model = Person         fields = ('first_name', 'last_name', 'favourite_food') 

So when i retrieve a Membership model using the MembershipSerializer, i get this json,

{     'id':1,     'url':'http://cheeselovers.com/api/member/1/'     'group':'http://cheeselovers.com/api/group/1/'     'date_joined': '2014-01-24T16:33:40.781Z',     'invite_reason': 'loves cheese',     'person':{         'first_name':'Barry',         'last_name':'CheeseLover',         'favourite_food': 'cheese'     } } 

but what i'd like returned is this,

{     'id':1,     'url':'http://cheeselovers.com/api/member/1/'     'group':'http://cheeselovers.com/api/group/1/'     'date_joined': '2014-01-24T16:33:40.781Z',     'invite_reason': 'loves cheese',     'first_name':'Barry',     'last_name':'CheeseLover',     'favourite_food': 'cheese' } 

Now i realise that i could simply accomplish this by changing the MembershipSerializer to this,

class MembershipSerializer(serializers.HyperlinkedModelSerializer):     first_name = serializers.Field(source='person.first_name')     last_name = serializers.Field(source='person.last_name')     favourite_food = serializers.Field(source='person.favourite_food')      class Meta:         model = Membership         fields = ('id', 'url', 'group', 'date_joined', 'invite_reason', 'first_name', 'last_name', 'favourite_food') 

BUT, the target model i have has 10 properties and the intermediary 'through' model only has read only props, so i already have a functioning serializer for the target model, that's used during the creation of the intermediary model.

It feels more DRY to be able to reuse this, so that if anything on the target model changes, i only have to make changes to it's serializer, for those changes to be then be reflected in the data returned by the intermediary's serializer.

So is there a way i can get the data from the PersonSerializer and add it to the Membership data, so that it's flat instead of nested?

...hope that all makes sense.

like image 547
james Avatar asked Jan 27 '14 13:01

james


2 Answers

Here's an approach based on James's answer but for a newer version of Django Rest Framework and support for reading and writing (update of the nested field only, it should be easy enough to add creation, see DRF's documentation for that.)

class ProfileSerializer(serializers.ModelSerializer):     class Meta:         model = Profile         fields = ('phone', 'some', 'other', 'fields')   class UserDetailsSerializer(serializers.ModelSerializer):     """User model with Profile. Handled as a single object, profile is flattened."""     profile = ProfileSerializer()      class Meta:         model = User         fields = ('username', 'email', 'profile')         read_only_fields = ('email', )      def to_representation(self, obj):         """Move fields from profile to user representation."""         representation = super().to_representation(obj)         profile_representation = representation.pop('profile')         for key in profile_representation:             representation[key] = profile_representation[key]          return representation      def to_internal_value(self, data):         """Move fields related to profile to their own profile dictionary."""         profile_internal = {}         for key in ProfileSerializer.Meta.fields:             if key in data:                 profile_internal[key] = data.pop(key)          internal = super().to_internal_value(data)         internal['profile'] = profile_internal         return internal      def update(self, instance, validated_data):         """Update user and profile. Assumes there is a profile for every user."""         profile_data = validated_data.pop('profile')         super().update(instance, validated_data)          profile = instance.profile         for attr, value in profile_data.items():             setattr(profile, attr, value)         profile.save()          return instance 
like image 63
ekuusela Avatar answered Sep 19 '22 14:09

ekuusela


James' answer is what I finally used. As I had several serializers using this method, I converted it to a mixin:

class FlattenMixin(object):     """Flatens the specified related objects in this representation"""     def to_representation(self, obj):         assert hasattr(self.Meta, 'flatten'), (             'Class {serializer_class} missing "Meta.flatten" attribute'.format(                 serializer_class=self.__class__.__name__             )         )         # Get the current object representation         rep = super(FlattenMixin, self).to_representation(obj)         # Iterate the specified related objects with their serializer         for field, serializer_class in self.Meta.flatten:             serializer = serializer_class(context = self.context)             objrep = serializer.to_representation(getattr(obj, field))             #Include their fields, prefixed, in the current   representation             for key in objrep:                 rep[field + "__" + key] = objrep[key]         return rep 

This way, you can do something like:

class MembershipSerializer(FlattenMixin, serializers.HyperlinkedModelSerializer):     class Meta:         model = Membership         fields = ('id', 'url', 'group', 'date_joined', 'invite_reason')         flatten = [ ('person', PersonSerializer) ] 
like image 32
julianjm Avatar answered Sep 23 '22 14:09

julianjm