Django rest framework, use different serializers in the same ModelViewSet
Asked Answered
B

9

315

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:

{
  "url": "http://127.0.0.1:8000/database/gruppi/2/",
  "nome": "universitari",
  "descrizione": "unitn!",
  "creatore": "emilio",
  "accesso": "CHI",
  "membri": [
    "emilio",
    "michele",
    "luisa",
    "ivan",
    "saverio"
  ]
}
  • 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.
    }
Bautista answered 24/3, 2014 at 17:54 Comment(5)
how did you implement it finaly? Using way proposed by user2734679 or using GenericAPIView?Eudoxia
As suggested by user2734679; I created a generic ViewSet adding a dictionary to specify the serializer for each action and a default serializer when not specifiedBautista
I have similar issue (#24810237) and for now ended with it(gist.github.com/andilab/a23a6370bd118bf5e858), but I am not very satisfied with it.Eudoxia
Created this small package for this. github.com/Darwesh27/drf-custom-viewsetsMarenmarena
Override retrieve method is OK.Katsuyama
T
439

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.                
Trumpeter answered 31/3, 2014 at 7:18 Comment(11)
This is great, thank you! I have overridden get_serializer_class thoughBautista
WARNING: django rest swagger does not place a self.action parameter, so this function will throw an exception. You might use gonz's answer or you might use if hasattr(self, 'action') and self.action == 'list'Counterweigh
Create a small pypi package for this. github.com/Darwesh27/drf-custom-viewsetsMarenmarena
How do we get the pk of object requested, if the action is retrieve?Piceous
My self.action is None. Could someone tell me why?Cesaro
@Cesaro And you're using DRF, and you're using a modelviewset?Trumpeter
@user2734679 sorry I was using Views and not ViewSets. Thank you.Cesaro
return serializers.Default - or just return super().get_serializer_class()Witchhunt
How do i specify an action? I have a get and details API... By default it takes list as the action.Guidon
@SiddharthPrajosh The action DRF provides depends on how the viewset got matched with the Django router. For the 'retrieve' action, you need to request a detail view (ex: /resource/3/) whereas the 'list' action would be the resource itself (ex: /resource/)Narayan
Maybe you need if self.request.method == "POST":.Pignut
R
110

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.

        """
        try:
            return self.serializer_action_classes[self.action]
        except (KeyError, AttributeError):
            return super(MultiSerializerViewSetMixin, self).get_serializer_class()
Rhapsodic answered 7/4, 2014 at 20:14 Comment(2)
Created this small package for this. github.com/Darwesh27/drf-custom-viewsetsMarenmarena
Too bad on the options request it always returns the default options (self.action = metadata), even for @actions on the viewset that have a different serializer.Waterbuck
T
54

This answer is the same as the accepted answer but I prefer to do in this way.

Generic views

get_serializer_class(self):

Returns the class that should be used for the serializer. Defaults to returning the serializer_class attribute.

May be overridden to provide dynamic behavior, such as using different serializers for reading and write operations or providing different serializers to the different types of users. the serializer_class attribute.

class DualSerializerViewSet(viewsets.ModelViewSet):
    # mapping serializer into the action
    serializer_classes = {
        'list': serializers.ListaGruppi,
        'retrieve': serializers.DettaglioGruppi,
        # ... other actions
    }
    default_serializer_class = DefaultSerializer # Your default serializer

    def get_serializer_class(self):
        return self.serializer_classes.get(self.action, self.default_serializer_class)
Tish answered 18/9, 2019 at 9:9 Comment(7)
Cannot use it because it tells me that my view has no attribute "action". It looks like ProductIndex(generics.ListCreateAPIView). Does it mean that you absolutely need to pass viewsets as argument or is there a way to do it using the generics API views?Grivation
a late reply to @Grivation comment - maybe someone can profit from that :) The example uses ViewSets, not Views :)Satire
So combined with this post #32589587, ViewSets seem to be the way to go to have more control over the different views and generate url automatically to have a consistent API? Originally thought that generics.ListeCreateAPIView was the most efficient, but too basic right?Grivation
Thanks for your answer Neo, I would like to know what is the best practice for handling permissions in django? Isn't it better to handle the permissions like what @Rhapsodic says?Tini
For this purpose, I have also seen this link, which maps the permission to actions using permission_classes_by_action field.Tini
@MostafaGhadimi, from the functionality perspective, there are the same. I prefer gonz's answer because whenever it fails to find a proper serializer for the action, it calls the default behavior of the serializer.Tish
nice one! Just in get_serializer_class I would return as return self.get_serializer_class.get(self.action) or myDefaultSerializer. and won't define the default_serializer_class.Mailemailed
H
18

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

Hallucinatory answered 16/11, 2017 at 21:36 Comment(1)
For the case in question, which is about using a different serializer for list and retrieve actions, you have the problem that both uses GET method. This is why django rest framework ViewSets uses the concept of actions, which is are similar, but slightly different than the corresponding http methods.Astronautics
M
11

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,
    }
Marenmarena answered 24/6, 2016 at 9:6 Comment(2)
It's better use mixin which much generic.Powel
Too bad on the options request it always returns the default options (self.action = metadata), even for @actions on the viewset that have a different serializer.Waterbuck
K
11

Just want to addon to existing solutions. If you want a different serializer for your viewset's extra actions (i.e. using @action decorator), you can add kwargs in the decorator like so:

@action(methods=['POST'], serializer_class=YourSpecialSerializer)
def your_extra_action(self, request):
    serializer = self.get_serializer(data=request.data)
    ...
Kristikristian answered 29/8, 2021 at 7:8 Comment(1)
Keep in mind that the action name to check for in the def get_serializer_class is that of your extra action method. In the example provided here, a check could look like self.action == 'your_extra_action'. Just want to specify it as it could differ from the related endpoint url which could be something like /your-extra-action.Grivation
S
2

With all other solutions mentioned, I was unable to find how to instantiate the class using get_serializer_class function and unable to find custom validation function as well. For those who are still lost just like I was and want full implementation please check the answer below.

views.py

from rest_framework.response import Response

from project.models import Project
from project.serializers import ProjectCreateSerializer, ProjectIDGeneratorSerializer


class ProjectViewSet(viewsets.ModelViewSet):
    action_serializers = {
        'generate_id': ProjectIDGeneratorSerializer,
        'create': ProjectCreateSerializer,
    }
    permission_classes = [IsAuthenticated]

    def get_serializer_class(self):
        if hasattr(self, 'action_serializers'):
            return self.action_serializers.get(self.action, self.serializer_class)

        return super(ProjectViewSet, self).get_serializer_class()

    # You can create custom function
    def generate_id(self, request):
        serializer = self.get_serializer_class()(data=request.GET)
        serializer.context['user'] = request.user
        serializer.is_valid(raise_exception=True)
        return Response(serializer.validated_data, status=status.HTTP_200_OK)

    def create(self, request, **kwargs):
        serializer = self.get_serializer_class()(data=request.data)
        serializer.context['user'] = request.user
        serializer.is_valid(raise_exception=True)
        return Response(serializer.validated_data, status=status.HTTP_200_OK)

serializers.py

import random
from rest_framework import serializers
from project.models import Project


class ProjectIDGeneratorSerializer(serializers.Serializer):
    def update(self, instance, validated_data):
        pass

    def create(self, validated_data):
        pass

    projectName = serializers.CharField(write_only=True)

    class Meta:
        fields = ['projectName']

    def validate(self, attrs):
        project_name = attrs.get('projectName')
        project_id = project_name.replace(' ', '-')

        return {'projectID': project_id}


class ProjectCreateSerializer(serializers.Serializer):
    def update(self, instance, validated_data):
        pass

    def create(self, validated_data):
        pass

    projectName = serializers.CharField(write_only=True)
    projectID = serializers.CharField(write_only=True)

    class Meta:
        model = Project
        fields = ['projectName', 'projectID']

    def to_representation(self, instance: Project):
        data = dict()
        data['projectName'] = instance.name
        data['projectID'] = instance.projectID
        data['createdAt'] = instance.createdAt
        data['updatedAt'] = instance.updatedAt

        representation = {
            'message': f'Project {instance.name} has been created.',
        }

        return representation

    def validate(self, attrs):
        print('attrs', dict(attrs))
        project_name = attrs.get('projectName')
        project_id = attrs.get('projectID')

        if Project.objects.filter(projectID=project_id).first():
            raise serializers.ValidationError(f'Project with ID {project_id} already exist')

        project = Project.objects.create(projectID=project_id,
                                         name=project_name)

        print('user', self.context['user'])
        project.user.add(self.context["user"])

        project.save()

        return self.to_representation(project)

urls.py

from django.urls import path

from .views import ProjectViewSet

urlpatterns = [
    path('project/generateID', ProjectViewSet.as_view({'get': 'generate_id'})),
    path('project/create', ProjectViewSet.as_view({'post': 'create'})),
]

models.py

# Create your models here.
from django.db import models

from authentication.models import User


class Project(models.Model):
    id = models.AutoField(primary_key=True)
    projectID = models.CharField(max_length=255, blank=False, db_index=True, null=False)
    user = models.ManyToManyField(User)
    name = models.CharField(max_length=255, blank=False)
    createdAt = models.DateTimeField(auto_now_add=True)
    updatedAt = models.DateTimeField(auto_now=True)

    def __str__(self):
        return self.name
Spevek answered 17/2, 2021 at 19:53 Comment(0)
T
1

Although pre-defining multiple Serializers in or way or another does seem to be the most obviously documented way, FWIW there is an alternative approach that draws on other documented code and which enables passing arguments to the serializer as it is instantiated. I think it would probably tend to be more worthwhile if you needed to generate logic based on various factors, such as user admin levels, the action being called, perhaps even attributes of the instance.

The first piece of the puzzle is the documentation on dynamically modifying a serializer at the point of instantiation. That documentation doesn't explain how to call this code from a viewset or how to modify the readonly status of fields after they've been initated - but that's not very hard.

The second piece - the get_serializer method is also documented - (just a bit further down the page from get_serializer_class under 'other methods') so it should be safe to rely on (and the source is very simple, which hopefully means less chance of unintended side effects resulting from modification). Check the source under the GenericAPIView (the ModelViewSet - and all the other built in viewset classes it seems - inherit from the GenericAPIView which, defines get_serializer.

Putting the two together you could do something like this:

In a serializers file (for me base_serializers.py):

class DynamicFieldsModelSerializer(serializers.ModelSerializer):
"""
A ModelSerializer that takes an additional `fields` argument that
controls which fields should be displayed.
"""

def __init__(self, *args, **kwargs):
    # Don't pass the 'fields' arg up to the superclass
    fields = kwargs.pop('fields', None)

    # Adding this next line to the documented example
    read_only_fields = kwargs.pop('read_only_fields', None)

    # Instantiate the superclass normally
    super(DynamicFieldsModelSerializer, self).__init__(*args, **kwargs)

    if fields is not None:
        # Drop any fields that are not specified in the `fields` argument.
        allowed = set(fields)
        existing = set(self.fields)
        for field_name in existing - allowed:
            self.fields.pop(field_name)

    # another bit we're adding to documented example, to take care of readonly fields 
    if read_only_fields is not None:
        for f in read_only_fields:
            try:
                self.fields[f].read_only = True
            exceptKeyError:
                #not in fields anyway
                pass

Then in your viewset you might do something like this:

class MyViewSet(viewsets.ModelViewSet):
    # ...permissions and all that stuff

    def get_serializer(self, *args, **kwargs):

        # the next line is taken from the source
        kwargs['context'] = self.get_serializer_context()

        # ... then whatever logic you want for this class e.g:
        if self.action == "list":
            rofs = ('field_a', 'field_b')
            fs = ('field_a', 'field_c')
        if self.action == “retrieve”:
            rofs = ('field_a', 'field_c’, ‘field_d’)
            fs = ('field_a', 'field_b’)
        #  add all your further elses, elifs, drawing on info re the actions, 
        # the user, the instance, anything passed to the method to define your read only fields and fields ...
        #  and finally instantiate the specific class you want (or you could just
        # use get_serializer_class if you've defined it).  
        # Either way the class you're instantiating should inherit from your DynamicFieldsModelSerializer
        kwargs['read_only_fields'] = rofs
        kwargs['fields'] = fs
        return MyDynamicSerializer(*args, **kwargs)

And that should be it! Using MyViewSet should now instantiate your MyDynamicSerializer with the arguments you'd like - and assuming your serializer inherits from your DynamicFieldsModelSerializer, it should know just what to do.

Perhaps its worth mentioning that it can makes special sense if you want to adapt the serializer in some other ways …e.g. to do things like take in a read_only_exceptions list and use it to whitelist rather than blacklist fields (which I tend to do). I also find it useful to set the fields to an empty tuple if its not passed and then just remove the check for None ... and I set my fields definitions on my inheriting Serializers to 'all'. This means no fields that aren't passed when instantiating the serializer survive by accident and I also don't have to compare the serializer invocation with the inheriting serializer class definition to know what's been included...e.g within the init of the DynamicFieldsModelSerializer:

# ....
fields = kwargs.pop('fields', ())
# ...
allowed = set(fields)
existing = set(self.fields)
for field_name in existing - allowed:
self.fields.pop(field_name)
# ....

NB If I just wanted two or three classes that mapped to distinct actions and/or I didn't want any specially dynamic serializer behaviour, I might well use one of the approaches mentioned by others here, but I thought this worth presenting as an alternative, particularly given its other uses.

Titograd answered 28/9, 2019 at 1:20 Comment(0)
E
0

You can map your all serializers with the action using a dictionary in class and then get them from the get_serializer_class method. Here is what I am using to get different serializers in different cases.

class RushesViewSet(viewsets.ModelViewSet):

    serializer_class = DetailedRushesSerializer
    queryset = Rushes.objects.all().order_by('ingested_on')
    permission_classes = (IsAuthenticated,)
    filter_backends = (filters.SearchFilter, 
        django_filters.rest_framework.DjangoFilterBackend, filters.OrderingFilter)
    pagination_class = ShortResultsSetPagination
    search_fields = ('title', 'asset_version__title', 
        'asset_version__video__title')

    filter_class = RushesFilter
    action_serializer_classes = {
        "create": RushesSerializer,
        "update": RushesSerializer,
        "retrieve": DetailedRushesSerializer,
        "list": DetailedRushesSerializer,
        "partial_update": RushesSerializer,
        }

    def get_serializer_context(self):
        return {'request': self.request}

    def get_serializer_class(self):
        try:
            return self.action_serializer_classes[self.action]
        except (KeyError, AttributeError):
            error_logger.error("---Exception occurred---")
            return super(RushesViewSet, self).get_serializer_class()
Easy answered 29/1, 2022 at 19:53 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.