Django rest framework,如何在同一个ModelViewSet中使用不同的序列化器?

312

我想提供两个不同的序列化程序,同时能够受益于ModelViewSet的所有便利设施:

  • 当查看对象列表时,我希望每个对象都有一个URL,该URL将重定向到其详细信息,并且每个其他关系都使用目标模型的__unicode __显示;

例如:

{
  "url": "http://127.0.0.1:8000/database/gruppi/2/",
  "nome": "universitari",
  "descrizione": "unitn!",
  "creatore": "emilio",
  "accesso": "CHI",
  "membri": [
    "emilio",
    "michele",
    "luisa",
    "ivan",
    "saverio"
  ]
}
  • 查看对象详情时,我想使用默认的HyperlinkedModelSerializer

示例:

{
  "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/"
  ]
}

我成功地按照以下方式使所有这些工作:

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.

基本上我检测用户何时请求列表视图或详细视图,并更改serializer_class以适应我的需求。然而,我对这段代码并不满意,它看起来像一个肮脏的黑客程序,最重要的是,如果两个用户同时请求一个列表和一个详细信息怎么办?

是否有更好的方法可以使用ModelViewSets来实现这一点,还是我必须回退使用GenericAPIView?

编辑:
以下是如何使用自定义基础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.
    }

你最终是如何实现的?使用用户2734679提出的方法还是使用GenericAPIView? - andilabs
根据用户user2734679的建议,我创建了一个通用的ViewSet并添加了一个字典来指定每个操作所需的序列化程序以及当未指定时的默认序列化程序。 - BlackBear
我遇到了类似的问题(https://dev59.com/3GAf5IYBdhLWcg3wSRHi),目前只能用这个解决方案(https://gist.github.com/andilab/a23a6370bd118bf5e858),但我并不是很满意。 - andilabs
1
为此创建了这个小包。https://github.com/Darwesh27/drf-custom-viewsets - Mahammad Adil Azeem
1
覆盖检索方法没问题。 - gzerone
9个回答

436

覆盖你的get_serializer_class方法。该方法用于在模型混合中检索适当的序列化程序类。

请注意,还有一个get_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.                

1
太好了,谢谢!不过我已经重写了get_serializer_class方法。 - BlackBear
28
警告:Django Rest Swagger 没有放置 self.action 参数,因此该函数将抛出异常。您可以使用 gonz 的答案或使用 if hasattr(self, 'action') and self.action == 'list' - Tom Leys
1
为此创建一个小的pypi包。https://github.com/Darwesh27/drf-custom-viewsets - Mahammad Adil Azeem
3
“return serializers.Default” 或者只是 “return super().get_serializer_class()”,您可以直接返回父类的 get_serializer_class() 方法。 - Milano
1
@SiddharthPrajosh DRF提供的操作取决于视图集如何与Django路由器匹配。对于'retrieve'操作,您需要请求详细视图(例如:/resource/3/),而'list'操作将是资源本身(例如:/resource/)。 - David M.
显示剩余6条评论

110

你可能会发现这个mixin很有用,它重写了get_serializer_class方法,允许你声明一个将动作和序列化器类映射的字典,或者使用通常的行为。

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()

为此创建了这个小包:https://github.com/Darwesh27/drf-custom-viewsets - Mahammad Adil Azeem
很遗憾,在选项请求中它总是返回默认选项(self.action = metadata),即使视图集上的@actions具有不同的序列化程序。 - gabn88

54

这个答案和被接受的答案一样,但我更喜欢用这种方式。

通用视图

get_serializer_class(self):

返回应该用于序列化器的类。默认情况下返回serializer_class属性。

可以重写以提供动态行为,例如对读取和写入操作使用不同的序列化器或为不同类型的用户提供不同的序列化器。

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)

无法使用它,因为它告诉我我的视图没有属性“action”。它看起来像是ProductIndex(generics.ListCreateAPIView)。这是否意味着您绝对需要将viewsets作为参数传递,还是有一种使用通用API视图的方法可以实现? - Seb
1
对@Seb的评论回复有些晚了 - 或许有人可以从中受益 :) 这个例子使用的是ViewSets,而不是Views :) - fanny
1
为此,我也看到了这个链接,它使用permission_classes_by_action字段将权限映射到操作。 - Mostafa Ghadimi
1
@MostafaGhadimi,从功能角度来看,它们是相同的。我更喜欢gonz的答案,因为每当它无法找到适当的序列化程序来处理操作时,它都会调用序列化程序的默认行为。 - Phoenix
1
不错!在 get_serializer_class 中,我会使用 return self.get_serializer_class.get(self.action) or myDefaultSerializer,而不定义 default_serializer_class - Ali Aref
显示剩余2条评论

18

18
对于这个案例,它涉及在“list”和“retrieve”操作中使用不同的序列化器,问题是两者都使用了“GET”方法。这就是Django Rest Framework ViewSets使用“动作”概念的原因,它们类似于相应的HTTP方法,但略有不同。 - Håken Lid

11

基于 @gonz 和 @user2734679 的答案,我创建了这个小型 Python 包,以 ModelViewset 的子类形式提供此功能。以下是它的工作原理。

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,
    }

6
最好使用更通用的mixin。 - iamsk
很遗憾,在选项请求中它总是返回默认选项(self.action = metadata),即使视图集上的@actions具有不同的序列化程序。 - gabn88

11

我想补充现有的解决方案。如果您想为视图集的额外操作 (即使用 @action 装饰器) 使用不同的序列化程序,您可以在装饰器中添加 kwargs,如下所示:

@action(methods=['POST'], serializer_class=YourSpecialSerializer)
def your_extra_action(self, request):
    serializer = self.get_serializer(data=request.data)
    ...

请记住,在 def get_serializer_class 中要检查的操作名称是您的额外操作方法的名称。在此提供的示例中,检查可能类似于 self.action == 'your_extra_action'。我只是想明确指定它,因为它可能与相关的端点URL不同,后者可能类似于 /your-extra-action - Seb

2

在所有提到的其他解决方案中,我无法找到如何使用get_serializer_class函数实例化类,也无法找到自定义验证函数。对于那些仍然像我一样迷失并想要完整实现的人,请查看下面的答案。

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

1

虽然以某种方式预定义多个序列化器似乎是最明显的文档化方式,但是有一种替代方法可以利用其他文档化代码,并且在实例化序列化器时允许传递参数。如果您需要基于各种因素(例如用户管理级别、调用的操作,甚至实例的属性)生成逻辑,则可能更值得尝试。

谜题的第一部分是关于在实例化点动态修改序列化器的文档。该文档未说明如何从视图集中调用此代码,也未说明如何在初始化后修改字段的只读状态 - 但这并不难。

第二部分 - get_serializer方法 也有文档记录 - (在 get_serializer_class 的“其他方法”下面),因此可以放心使用(源代码非常简单,这意味着修改时不太可能出现意外副作用)。请查看 GenericAPIView 下的源代码(ModelViewSet - 和所有其他内置视图集类似 - 继承自定义 get_serializer 方法。

将两者结合起来,您可以像这样做:

在序列化文件中(对于我来说是 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

在您的视图集中,您可能会这样做:

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)

就是这样!使用MyViewSet现在应该使用您想要的参数实例化MyDynamicSerializer,并且假设您的序列化器从DynamicFieldsModelSerializer继承,它应该知道该怎么做。

也许值得一提的是,如果您想以其他方式适应序列化器...例如执行诸如接受read_only_exceptions列表并将其用于白名单而不是黑名单字段等操作时,它可以特别有意义。 我还发现,如果未传递字段,则将字段设置为空元组对我很有用,然后只需删除None的检查即可...并将我的字段定义设置为'inheriting Serializers'上的'all'。 这意味着在实例化序列化器时不会意外保留任何未传递的字段,我也不必比较序列化器调用与继承的序列化器类定义以了解包含了什么...例如在DynamicFieldsModelSerializer的init中:

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

如果我只需要两到三个映射到不同操作的类,并且我不需要任何特别动态的序列化器行为,那么我可能会使用其他人在这里提到的方法之一,但是考虑到其它用途,我认为这个方案也值得呈现。

0

您可以使用类中的字典将所有序列化器映射到操作,然后从“get_serializer_class”方法中获取它们。以下是我在不同情况下获取不同序列化器所使用的内容。

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()

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接