Search code examples
django-rest-framework

Django REST Framework: make a serializer behave different for request payload vs response body?


I'm trying to come up with an easy way to make my serializers and views work the way I want to, while preventing a bunch of manual boilerplate.

My simplified model:

class Character(models.Model):
    name = models.CharField(max_length=255)
    location = models.ForeignKey(Location, on_delete=models.SET_NULL, blank=True, null=True)
    factions = models.ManyToManyField(Faction, blank=True)

So: character can have one location, and multiple factions.

Now, when I GET a character, I'd like those submodels (Location and Faction) to be expanded to their full representation, so my serializer looks like this:

class CharacterSerializer(serializers.ModelSerializer):
    location = LocationSerializer(read_only=True)
    factions = FactionSerializer(many=True, read_only=True)

    class Meta:
        model = Character
        fields = "__all__"

And this works perfectly fine so far. The thing is, when I POST or PUT a character, I'd like to just send the id(s).. but still get the full location and faction objects back in the response.

In other words, when I create a character with a payload like this:

{"name":"Saga","location":1,"factions":[1]}

I'd like the response to look like this:

{
  "id": 1,
  "location": {
    "id": 1,
    "name": "Location 1"
  },
  "factions": [
    {
      "id": 1,
      "name": "Faction 1"
    }
  ],
  "name": "Saga"
}

Is this possible at all without overriding the create and update methods of my ModelViewSet subclass? I was hoping I could slightly modify the serializer itself to only apply those location and factions field serializers on the response, not on the request.


Solution

  • What I came up so far was to create my own ModelViewSet subclass:

    class DualSerializerModelViewSet(viewsets.ModelViewSet):
        def create(self, request, *args, **kwargs):
            write_serializer = self.write_serializer_class(data=request.data)
            write_serializer.is_valid(raise_exception=True)
            created_object = self.perform_create(write_serializer)
            headers = self.get_success_headers(write_serializer.data)
    
            read_serializer = self.serializer_class(created_object)
            return Response(read_serializer.data, status=status.HTTP_201_CREATED, headers=headers)
    
        def update(self, request, *args, **kwargs):
            partial = kwargs.pop("partial", False)
            instance = self.get_object()
            write_serializer = self.write_serializer_class(instance, data=request.data, partial=partial)
            write_serializer.is_valid(raise_exception=True)
            self.perform_update(write_serializer)
    
            if getattr(instance, "_prefetched_objects_cache", None):
                # If 'prefetch_related' has been applied to a queryset, we need to
                # forcibly invalidate the prefetch cache on the instance.
                instance._prefetched_objects_cache = {}
    
            read_serializer = self.serializer_class(instance)
            return Response(read_serializer.data)
    

    It can be used as such:

    class WriteCharacterSerializer(serializers.ModelSerializer):
        class Meta:
            model = Character
            fields = "__all__"
    
    
    class CharacterSerializer(WriteCharacterSerializer):
        location = LocationSerializer(read_only=True)
        factions = FactionSerializer(many=True, read_only=True)
    
    
    class CharacterController(DualSerializerModelViewSet):
        serializer_class = CharacterSerializer
        write_serializer_class = WriteCharacterSerializer
        queryset = Character.objects.all()
    

    This works pretty great for my use case, maybe it helps someone else as well.