Django REST Framework: creating hierarchical objects using URLs

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

Should I just have you send POST to /v1/subobjects/ with a parent in the payload to add subobjects, or is there a clean way to pass pk kwarg from the url to the serializer? I use HyperlinkedModelSerializer and ModelViewSet as my respective base classes. Is there a recommended way to do this? So far, the only idea I had was to completely reimplement ViewSets and create a custom Serializer class, get_default_fields () comes from a dictionary that is passed from the ViewSet populated by its kwargs. It seems pretty attractive to something that I would think it is fully up and running, so I can't help but think that I'm missing something. Every REST API I've ever seen that has writable endpoints has that kind of URL-based output, so the fact that the django-rest-framework doesn't seem to be able to do this seems weird.

+6
source share
4 answers

Make the parent object a serializer of the read_only field. This is not optional, but it does not come from the request data either. Instead, you pull 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 
+3
source

Here is what I did to solve it, although it would be nice if there was a more general way to do this, since it is such a common URL pattern. First I created a mixin for my ViewSets, which overrided 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) 

It is basically copied and pasted from CreateModelMixin , but it defines an initial_instance method that we can override in subclasses to provide a starting point for a serializer that is configured for 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 understand that I really do not need to do .get on pk to associate it with the model, but in my case, I open slug, not the primary key in the public API)

+1
source

If you use ModelSerializer (which is implemented by HyperlinkedModelSerializer ), it is as simple 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() used to deserialize an attribute dictionary into an instance of an object. ModelSerializer implements this method and creates / updates an instance for the model specified in the Meta class. If the given instance is None , this means that the object has yet to be created, so you simply add the parent_id attribute to the attrs argument and call super() .

This way you do not need to specify a read-only field or have a custom view / serializer.

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

0
source

It may be a little late, but I think this library of drf nested libraries may be useful for this operation.

0
source

Source: https://habr.com/ru/post/950976/


All Articles