1

I'm trying to write a Django Rest Framework serializer. It is supposed to take in a simple primitive data and return data with some of the related objects data. The model I'm serializing is Model_C

Here are the models:

class ModelA(models.Model):
    name = models.Charfield(max_length=16)
    attr1 = models...
    ...

class ModelB(models.Model):
    name = models.CharField(max_length=16)
    attr1 = models...
    ...

class ModelC(models.Model):
    model_a = models.ForeignKey(ModelA)
    model_b = models.ForeignKey(ModelB)
    text = models.CharField(max_length=32)
    date = models.DateTimeField()

Here is the api view:

class ModelCApiView(RetrieveUpdateDestroyAPIView):
    serializer_class = ModelCSerializer

    def get_queryset(self):
        return self.serializer_class.Meta.model.objects.all()

    def post(self, request):
        serializer = self.get_serializer(data=request.data)
        if serializer.is_valid():
            serializer.save()
            return Response(data=serializer.data, status=status.HTTP_201_CREATED)
        else:
            return Response(data=serializer.errors, status=status.HTTP_400_BAD_REQUEST)

The data fed to the serializer are as follows (with or without the ID):

{'id': '1', 'model_a': 1, 'model_b': 1, 'text': 'some text', 'date': '2019-08-28 08:00:00'}

Now, the thing is that the serializer should save the instance and return the serialized model WITH SOME OF THE related data:

{'id': '1', 'model_a': {'id': 1, 'name': 'some model a name'}, 'model_b': {'id': 1, 'name': 'some model b name', 'attr1': 'some attr value'}, 'text': 'some text', 'date': '2019-08-28 08:00:00'}

So the only thing that is created is ModelC instance. The api view is not supposed to create ModelA or ModelB instances.

The main problem is with serializing/deserializing objects. I already tried several things:

  1. The most simple
class ModelCSerializer(serializers.ModelSerializer):
    class Meta:
        model = ModelC
        fields = "__all__"

It properly deserializes the request, saves instance, but returns too little information - only IDs of related objects.


  1. I used depth
class ModelCSerializer(serializers.ModelSerializer):
    class Meta:
        model = ModelC
        fields = "__all__"
        depth = 1

This does not work, as it spits out 500 with error: "Column 'model_a_id' cannot be null".


  1. I tried adding to 2. declared serializers:
class ModelCSerializer(serializers.ModelSerializer):
    class Meta:
        model = ModelC
        fields = "__all__"
        depth = 1
    model_a = ModelASerializer()
    model_b = ModelBSerializer()

This produces error 400, bad request, as it asks me to insert ALL data for model_a and model_b as a dict.


  1. I added lines to APIView to serialize data with simple serializer, and then serialize again with more advanced and return more info.
    serializer = ModelCSerializer

    def post(self, request):
        serializer = self.get_serializer(data=request.data)
        if serializer.is_valid():
            obj = serializer.save()
            serializer = ModelCReadOnlySerializer(instance=obj, data=serializer.data)
            serializer.is_valid()
            return Response(data=serializer.data, status=status.HTTP_201_CREATED)
        else:
            return Response(data=serializer.errors, status=status.HTTP_400_BAD_REQUEST)


class ModelCReadOnlySerializer(serializers.ModelSerializer):
    class Meta:
        model = ModelC
        fields = ("id", "model_a", "model_b", "text", "date")
        read_only_fields = fields
        depth = 1

This is the closest to the solution as I have been, since it returns almost what I need, except for that it returns ALL data for model_a and model_b, which is way too much.


At this point I have other ideas, but I bet they're all terrible. I'd be glad on any hints how to proceed, because I'm this close -> || <- to writing everything ny myself without DRF :-)

Thank you in advance.

Eric Luther
  • 117
  • 1
  • 10
  • For me, the very first vague thing is you are using 'post' method in RetrieveUpdate Destroy View . This class is used for deleting, updating and retrieving (delete, put,patch, ger) not for creating object and you are using post method on it, which should not be allowed. You should have been using ListAPIView or ListCreateAPIView – Nimish Bansal Aug 30 '19 at 13:59
  • Possible duplicate of [Dynamically exclude or include a field in Django REST framework serializer](https://stackoverflow.com/questions/27935558/dynamically-exclude-or-include-a-field-in-django-rest-framework-serializer) – Nimish Bansal Aug 30 '19 at 14:05
  • For your read-only serializer, you should still use the ModelASerializer and ModelBSerializer in order to specify which fields you want to return. I think that's the small tweak you need on method 4. – dirkgroten Aug 30 '19 at 14:14
  • @dirkgroten I tried that just now. The serializer now returns ONLY the fields specified as serializers, completely disregarding 'fields' Meta: {"model_a": 1, "model_b"} – Eric Luther Aug 30 '19 at 14:19
  • you shouldn't pass it data because you're just serialising an existing object. – dirkgroten Aug 30 '19 at 14:22
  • @dirkgroten Holy *^&% it works! Thanks a lot! I thought you always have to add some data. Great job, thank you very much! – Eric Luther Aug 30 '19 at 14:28
  • Now in terms of approaches, another, possibly cleaner approach (but you'd have to see it to know if it's more readable) would be to create the default writable serializer and [override its deserialisation](https://www.django-rest-framework.org/api-guide/serializers/#overriding-serialization-and-deserialization-behavior) behaviour only by overriding `to_representation()`. – dirkgroten Aug 30 '19 at 14:33
  • @dirkgroten I'd say it is about the same readability, so it's a better idea, since there is no other purpose of having two different serializers for that one model. Thank you again for your help! – Eric Luther Sep 02 '19 at 09:03

2 Answers2

0

With great help from @dirkgroten:

Number four is the way to go. First, there have to be two serializers: one for write (as per 1.) and one for read:

class ModelCReadOnlySerializer(serializers.ModelSerializer):
    class Meta:
        model = ModelC
        fields = ("id", "model_a", "model_b", "text", "date")
        read_only_fields = fields
    # Not there is no depth any more
    model_a = ModelASerializer()
    model_b = ModelBSerializer()

And in the view, data does not need to be passed into read-only serializer:

if serializer.is_valid():
            obj = serializer.save()
            serializer = ModelCReadOnlySerializer(instance=obj)
            return Response(data=serializer.data, status=status.HTTP_201_CREATED)
        else:
            return Response(data=serializer.errors, status=status.HTTP_400_BAD_REQUEST)

Now the data is received in simple form, processed and saved, and then more details are returned.

Great thanks once more!

Eric Luther
  • 117
  • 1
  • 10
0

Another way is to override .to_representation(instance) method on the serializer to customize the way output is returned. It takes a little more space, makes little less readable, but makes it more consise and puts all serialization logic into one serializer.

Eric Luther
  • 117
  • 1
  • 10