Take the 2-minute tour ×
Stack Overflow is a question and answer site for professional and enthusiast programmers. It's 100% free, no registration required.

I have a django-rest-framework REST API with hierarchical resources. I want to be able to create subobjects by POSTing to /v1/objects/<pk>/subobjects/ and have it automatically set the foreign key on the new subobject to the pk kwarg from the URL without having to put it in the payload. Currently, the serializer is causing a 400 error, because it expects the object foreign key to be in the payload, but it shouldn't be considered optional either. The URL of the subobjects is /v1/subobjects/<pk>/ (since the key of the parent isn't necessary to identify it), so it is still required if I want to PUT an existing resource.

Should I just make it so that you POST to /v1/subobjects/ with the parent in the payload to add subobjects, or is there a clean way to pass the pk kwarg from the URL to the serializer? I'm using HyperlinkedModelSerializer and ModelViewSet as my respective base classes. Is there some recommended way of doing this? So far the only idea I had was to completely re-implement the ViewSets and make a custom Serializer class whose get_default_fields() comes from a dictionary that is passed in from the ViewSet, populated by its kwargs. This seems quite involved for something that I would have thought is completely run-of-the-mill, so I can't help but think I'm missing something. Every REST API I've ever seen that has writable endpoints has this kind of URL-based argument inference, so the fact that django-rest-framework doesn't seem to be able to do it at all seems strange.

share|improve this question

3 Answers 3

up vote 2 down vote accepted

Make the parent object serializer field read_only. It's not optional but it's not coming from the request data either. Instead you pull the pk/slug from the URL in pre_save()...

# Assuming list and detail URLs like:
#   /v1/objects/<parent_pk>/subobjects/
#   /v1/objects/<parent_pk>/subobjects/<pk>/
def pre_save(self, obj):
    parent = models.MainObject.objects.get(pk=self.kwargs['parent_pk'])
    obj.parent = parent
share|improve this answer
    
pre_save doesn't get called until after deserialization, at which point it has already thrown a validation error. –  Volte Aug 4 '13 at 17:35
    
Ah, okay. Answer updated. –  Carlton Gibson Aug 4 '13 at 19:24
    
That is one option and would work for something like comments, but some of the objects I have should be able to be reparented, so setting the parent explicitly in a PUT would be nice too. On the other hand, I could do it this way and then have an explicit 'reparent' POST action or something, but that seems a little less RESTful. I'll accept this answer since it's a good idea and will work for the majority of cases. –  Volte Aug 4 '13 at 19:29

Here's what I've done to solve it, although it would be nice if there was a more general way to do it, since it's such a common URL pattern. First I created a mixin for my ViewSets that redefined the create method:

class CreatePartialModelMixin(object):
    def initial_instance(self, request):
        return None

    def create(self, request, *args, **kwargs):
        instance = self.initial_instance(request)
        serializer = self.get_serializer(
            instance=instance, data=request.DATA, files=request.FILES,
            partial=True)

        if serializer.is_valid():
            self.pre_save(serializer.object)
            self.object = serializer.save(force_insert=True)
            self.post_save(self.object, created=True)
            headers = self.get_success_headers(serializer.data)
            return Response(
                serializer.data, status=status.HTTP_201_CREATED,
                headers=headers)

        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

Mostly it is copied and pasted from CreateModelMixin, but it defines an initial_instance method that we can override in subclasses to provide a starting point for the serializer, which is set up to do a partial deserialization. Then I can do, for example,

class SubObjectViewSet(CreatePartialModelMixin, viewsets.ModelViewSet):
    # ....

    def initial_instance(self, request):
        instance = models.SubObject(owner=request.user)
        if 'pk' in self.kwargs:
            parent = models.MainObject.objects.get(pk=self.kwargs['pk'])
            instance.parent = parent
        return instance

(I realize I don't actually need to do a .get on the pk to associate it on the model, but in my case I'm exposing the slug rather than the primary key in the public API)

share|improve this answer

If you're using ModelSerializer (which is implemented by HyperlinkedModelSerializer) it's as easy as implementing the restore_object() method:

class MySerializer(serializers.ModelSerializer):

    def restore_object(self, attrs, instance=None):

        if instance is None:
            # If `instance` is `None`, it means we're creating
            # a new object, so we set the `parent_id` field.
            attrs['parent_id'] = self.context['view'].kwargs['parent_pk']

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

    # ...

restore_object() is used to deserialize a dictionary of attributes into an object instance. ModelSerializer implements this method and creates/updates the instance for the model you specified in the Meta class. If the given instance is None it means the object still has to be created, so you just add the parent_id attribute on the attrs argument and call super().

So this way you don't have to specify a read-only field, or have a custom view/serializer.

More information: http://www.django-rest-framework.org/api-guide/serializers#declaring-serializers

share|improve this answer

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

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