Home » Django » Django rest framework, use different serializers in the same ModelViewSet

Django rest framework, use different serializers in the same ModelViewSet

Posted by: admin November 30, 2017 Leave a comment

Questions:

I would like to provide two different serializers and yet be able to benefit from all the facilities of ModelViewSet:

  • When viewing a list of objects, I would like each object to have an url which redirects to its details and every other relation appear using _ _ unicode _ _ of the target model; example:
    [
        {
            "membri": [
                "emilio", 
                "michele", 
                "luisa", 
                "ivan", 
                "saverio"
            ],
            "creatore": "emilio", 
            "url": "http://127.0.0.1:8000/database/gruppi/2/", 
            "nome": "universitari", 
            "descrizione": "unitn!", 
            "accesso": "CHI"
        }
    ]  
    
  • When viewing the details of an object, I would like to use the default HyperlinkedModelSerializer, example:
    {
        "url": "http://127.0.0.1:8000/database/gruppi/2/", 
        "nome": "universitari", 
        "descrizione": "unitn!", 
        "creatore": "http://127.0.0.1:8000/database/utenti/3/", 
        "accesso": "CHI", 
        "membri": [
            "http://127.0.0.1:8000/database/utenti/3/", 
            "http://127.0.0.1:8000/database/utenti/4/", 
            "http://127.0.0.1:8000/database/utenti/5/", 
            "http://127.0.0.1:8000/database/utenti/6/", 
            "http://127.0.0.1:8000/database/utenti/7/"
        ]
    }

I managed to make all this work as I wish in the following way:

serializers.py

# serializer to use when showing a list
class ListaGruppi(serializers.HyperlinkedModelSerializer):
    membri = serializers.RelatedField(many = True)
    creatore = serializers.RelatedField(many = False)

    class Meta:
        model = models.Gruppi

# serializer to use when showing the details
class DettaglioGruppi(serializers.HyperlinkedModelSerializer):
    class Meta:
        model = models.Gruppi

views.py

class DualSerializerViewSet(viewsets.ModelViewSet):
    """
    ViewSet providing different serializers for list and detail views.

    Use list_serializer and detail_serializer to provide them
    """
    def list(self, *args, **kwargs):
        self.serializer_class = self.list_serializer
        return viewsets.ModelViewSet.list(self, *args, **kwargs)

    def retrieve(self, *args, **kwargs):
        self.serializer_class = self.detail_serializer
        return viewsets.ModelViewSet.retrieve(self, *args, **kwargs)

class GruppiViewSet(DualSerializerViewSet):
    model = models.Gruppi
    list_serializer = serializers.ListaGruppi
    detail_serializer = serializers.DettaglioGruppi

    # etc.

Basically I detect when the user is requesting a list view or a detailed view and change serializer_class to suit my needs. I am not really satisfied with this code though, it looks like a dirty hack and, most importantly, what if two users request a list and a detail at the same moment?

Is there a better way to achieve this using ModelViewSets or do I have to fall back using GenericAPIView?

EDIT:
Here’s how to do it using a custom base ModelViewSet:

class MultiSerializerViewSet(viewsets.ModelViewSet):
    serializers = { 
        'default': None,
    }

    def get_serializer_class(self):
            return self.serializers.get(self.action,
                        self.serializers['default'])

class GruppiViewSet(MultiSerializerViewSet):
    model = models.Gruppi

    serializers = {
        'list':    serializers.ListaGruppi,
        'detail':  serializers.DettaglioGruppi,
        # etc.
    }
Answers:

Override your get_serializer_class method. This method is used in your model mixins to retrieve the proper Serializer class.

Note that there is also a get_serializer method which returns an instance of the correct Serializer

class DualSerializerViewSet(viewsets.ModelViewSet):
    def get_serializer_class(self):
        if self.action == 'list':
            return serializers.ListaGruppi
        if self.action == 'retrieve':
            return serializers.DettaglioGruppi
        return serializers.Default # I dont' know what you want for create/destroy/update.                

Questions:
Answers:

You may find this mixin useful, it overrides the get_serializer_class method and allows you to declare a dict that maps action and serializer class or fallback to the usual behavior.

class MultiSerializerViewSetMixin(object):
    def get_serializer_class(self):
        """
        Look for serializer class in self.serializer_action_classes, which
        should be a dict mapping action name (key) to serializer class (value),
        i.e.:

        class MyViewSet(MultiSerializerViewSetMixin, ViewSet):
            serializer_class = MyDefaultSerializer
            serializer_action_classes = {
               'list': MyListSerializer,
               'my_action': MyActionSerializer,
            }

            @action
            def my_action:
                ...

        If there's no entry for that action then just fallback to the regular
        get_serializer_class lookup: self.serializer_class, DefaultSerializer.

        Thanks gonz: http://stackoverflow.com/a/22922156/11440

        """
        try:
            return self.serializer_action_classes[self.action]
        except (KeyError, AttributeError):
            return super(MultiSerializerViewSetMixin, self).get_serializer_class()

Questions:
Answers:

Based on @gonz and @user2734679 answers I’ve created this small python package that gives this functionality in form a child class of ModelViewset. Here is how it works.

from drf_custom_viewsets.viewsets.CustomSerializerViewSet
from myapp.serializers import DefaltSerializer, CustomSerializer1, CustomSerializer2

class MyViewSet(CustomSerializerViewSet):
    serializer_class = DefaultSerializer
    custom_serializer_classes = {
        'create':  CustomSerializer1,
        'update': CustomSerializer2,
    }

Questions:
Answers:

Regarding providing different serializers, why is nobody going for the approach that checks the HTTP method? It’s clearer IMO and requires no extra checks.

def get_serializer_class(self):
    if self.request.method == 'POST':
        return NewRackItemSerializer
    return RackItemSerializer

Credits/source: https://github.com/encode/django-rest-framework/issues/1563#issuecomment-42357718