26

I'm trying POST a new a Nested Object, the problem is just create the "top" object (Playlist), but don't create the "ChannelItem"...

My Models:

class Playlist(models.Model):
    provider = models.IntegerField()
    channel_id = models.CharField(max_length=100)
    channel_version = models.CharField(blank=True, max_length=100)
    start = models.DateTimeField()
    url = models.CharField(max_length=500)


class ChannelItem(models.Model):
    playlist = models.ForeignKey(Playlist, editable=False, related_name='channelitems')
    content_id = models.CharField(max_length=100)
    content_version = models.CharField(blank=True, max_length=100)

My Serializer:

class ChannelItemSerializer(serializers.ModelSerializer):
    class Meta:
        model = ChannelItem
        fields = ('content_id', 'content_version')
        exclude = ('id')
        depth = 1


class PlaylistSerializer(serializers.ModelSerializer):

    class Meta:
        model = Playlist
        fields = ('id', 'provider', 'channel_id', 'channel_version', 'start', 
                  'url', 'channelitems')
        depth = 2

channelitems = ChannelItemSerializer()

I use the curl to post the following data :

'{"provider":125,"channel_id":"xyz", "channel_version":"xsqt", 
"start":"2012-12-17T11:04:35","url":"http://192.168.1.83:8080/maaaaa",
"channelitems":[{"content_id":"0.flv", "content_version":"ss"},
{"content_id":"1.flv","content_version":"ss"}]}' http://localhost:8000/playlist_scheduler/playlists/

I receive the message:

HTTP/1.1 201 CREATED
Content-Type: application/json
Transfer-Encoding: chunked
Date: Mon, 17 Dec 2012 20:12:54 GMT
Server: 0.0.0.0

{"id": 25, "provider": 125, "channel_id": "xyz", "channel_version": "xsqt",
"start":"2012-12-17T11:04:35", "url": "http://localhost:8080/something",
"channelitems": []}

4 Answers 4

22

Nested representations do not currently support read-write, and should instead be read-only.

You should probably look into using a flat representation instead, using pk or hyperlinked relations.

If you need the nested representation, you may want to consider having two separate endpoints - a flat writable endpoint, and a nested read-only endpoint.

8
  • 7
    I've seen the term flat representation also used several times in the REST framework documentation, but can you give an example of what this means exactly? I fully understand the nested serializers, but I need to read-write, not just read. My options it seems are to flatten out the representation, or to write my own custom RelatedField?
    – user798719
    Commented Jul 5, 2013 at 4:41
  • @avances123 it seems so! see: github.com/tomchristie/django-rest-framework/blob/master/…
    – fjsj
    Commented Apr 13, 2014 at 1:36
  • @fjsj the link you posted is returning 404. Can anyone post a link that shows where/how it says to do this? Commented Nov 21, 2014 at 16:42
  • 1
    @JamesBerlin it seems the file was moved. here is the current link: github.com/tomchristie/django-rest-framework/blob/… but those tests are commented out. don't know why.
    – fjsj
    Commented Nov 22, 2014 at 18:14
  • @TomChristie is nested representation supported now? Could you provide a link on how to use it? Commented Nov 23, 2014 at 19:33
6

If someone needs a quick-and-dirty solution for that, I came up with this one I'll be temporary using in a project:

class NestedManyToManyField(serializers.WritableField):
    def to_native(self, value):
        serializer = self.Meta.serializer(value.all(), many=True, context=self.context)
        return serializer.data
    def from_native(self, data):
        serializer = self.Meta.serializer(data=data, many=True, context=self.context)
        serializer.is_valid()
        serializer.save()
        return serializer.object
    class Meta:
        serializer = None

Then create your own subclass of NestedManyToManyField:

class TopicNestedSerializer(NestedManyToManyField):
    class Meta:
        serializer = MyOriginalSerializer

An example of MyOriginalSerializer:

class MyOriginalSerializer(serializers.ModelSerializer):
    class Meta:
        model = models.MyModel
        fields = ('id', 'title',)

This works fine for me so far. But be aware there are clean fixes coming:

3

after a long effort I made a first version that funcinasse ... I believe that with some improvement could be included within the ModelSerializer

class ChannelItemSerializer(serializers.ModelSerializer):

    class Meta:
        model = ChannelItem
        fields = ('id', 'content_id', 'content_version')

    def field_from_native(self, data, files, field_name, into):
        try:
            if self._use_files:
                _files = files[field_name]
            else:
                _data = data[field_name]
        except KeyError:
            if getattr(self, 'default', None):
                _data = self.default
            else:
                if getattr(self, 'required', None):
                    raise ValidationError(self.error_messages['required'])
                return

        if type(_data) is list:
            into[field_name] = [] 
            for item in _data:
                into[field_name].append(self._custom_from_native(item))
        else:
            into[field_name] = self._custom_from_native(_data)


    def _custom_from_native(self, data):
        self._errors = {}
        if data is not None:
            attrs = self.restore_fields(data, None)
            attrs = self.perform_validation(attrs)
        else:
            self._errors['non_field_errors'] = ['No input provided']

        if not self._errors:
            return self.restore_object(attrs, instance=getattr(self, 'object', None))




class PlaylistSerializer(serializers.ModelSerializer):

    class Meta:
        model = Playlist
        fields = ('id', 'provider', 'channel_id', 'channel_version', 'start', 'url', 'channel_items')
        depth = 1

    channel_items = ChannelItemSerializer()

    def restore_object(self, attrs, instance=None):
        self.foreign_data = {}

        for (obj, model) in self.opts.model._meta.get_all_related_objects_with_model():
            field_name = obj.field.related_query_name()
            if field_name in attrs:
                self.foreign_data[field_name] = attrs.pop(field_name)


        return super(PlaylistSerializer, self).restore_object(attrs, instance)

    def save(self, save_m2m=True):
        super(PlaylistSerializer, self).save(save_m2m)

        if getattr(self, 'foreign_data', None):
            for accessor_name, object_list in self.foreign_data.items():
                setattr(self.object, accessor_name, object_list)
            self.foreign_data = {}

        return self.object
0
1

For me, I have a hybrid workaround that I'm OK with. Namely, create a view that has:

  • the ManyToMany field in its un-nested serializer form
  • alias the nested ManyToMany field into a variable with _objs as the suffix and specify it as as read only
  • when you PUT back to the server reconcile the two aliased fields and store the result in the un-nested serializer field

e.g.

class MSerializer(serializers.HyperlinkedModelSerializer):
    foo_objs = TempSensorSerializer(source='foos', many=True, allow_add_remove=True,required=False,read_only=True)
    class Meta:
        model = M
        fields = ('url', 'foos', 'foo_objs')

I don't love this solution, but it beats trying to separately query and collate the nested fields after retrieving the initial container.

Not the answer you're looking for? Browse other questions tagged or ask your own question.