Django REST框架POST嵌套对象

50

我现在遇到一个小问题,涉及到 Django Rest Framework。我正在尝试发布一个包含嵌套对象的对象。

这是我的 serializers.py

class ClassSerializer(serializers.ModelSerializer):
    class Meta:
        model = Class
        fields = ('number', 'letter')


class SubjectSerializer(serializers.ModelSerializer):
    class Meta:
        model = Subject
        fields = ('title',)


class ExamSerializer(serializers.ModelSerializer):
    subject = SubjectSerializer()
    clazz = ClassSerializer()

    class Meta:
        model = Exam
        fields = ('id', 'subject', 'clazz', 'topic', 'date', 'details')
        depth = 1

    def create(self, validated_data):
        return Exam.objects.create(**validated_data)

    def update(self, instance, validated_data):
        instance.__dict__.update(**validated_data)
        instance.save()

        return instance

并且从views.py中创建create()函数:

def create(self, request):
    serializer = self.serializer_class(data=request.data)
    serializer.is_valid(raise_exception=True)
    self.perform_create(serializer)

    return Response(serializer.validated_data, status=status.HTTP_201_CREATED)

这是Postman的响应结果: Postman response

我在这里看到了一些关于这个问题的帖子,但我仍然无法解决它。我已经尝试了几种方法来修复它,但它仍然返回"This field is required."


这是一个常见的问题,看看我的答案,你会发现它很有用。http://stackoverflow.com/questions/41308406/django-rest-framework-add-object-to-request-data-and-then-call-serializer-is-va - Ivan Semochkin
5个回答

78

您正在处理嵌套序列化的问题。在继续之前,请阅读链接的文档。

您的问题涉及DRF中一个复杂的问题领域,因此需要一些解释和讨论,以了解序列化器和视图集如何工作。

我将讨论通过使用不同的数据表示方式来表示您的SubjectClass数据的相同端点的问题,因为这通常是人们希望以嵌套格式表示其数据时遇到的问题;他们希望为其用户界面提供足够的信息以进行清洁使用,例如通过下拉选择器。

默认情况下,Django和Django REST Framework(DRF)通过它们的主键引用相关对象(您的SubjectClass)。这些默认情况下是自动递增整数键与Django。如果您想以其他方式引用它们,则必须编写覆盖。有几个不同的选项。

  1. 第一种选择是专门化您的创建和更新逻辑:通过某些其他属性引用您的类,并手动编写创建的查找,或将您正在引用的键设置为类的主键。您可以将类的名称、UUID或任何其他属性设置为主数据库键,只要它是唯一的,单个字段(我之所以提到这一点,是因为您目前正在使用由复合(数字、字母)搜索项组成的复合搜索来查找您的Class模型)。您可以在create视图方法中覆盖相关对象查找(对于POST),但那么您还必须处理update视图方法中的类似查找(对于PUT和PATCH)。
  2. 其次,在我看来,更可取的选择是专门化您的对象表示:通过主键通常引用您的类,并创建一个用于读取对象的序列化器和一个用于创建和更新对象的序列化器。这可以通过序列化器类继承和覆盖表示轻松实现。在您的POST、PUT、PATCH等请求中使用主键更新您的类引用和外键。

选项1:在创建和更新时使用任意属性查找类和主题:
将嵌套类序列化器设置为只读。
class ExamSerializer(serializers.ModelSerializer):
    subject = SubjectSerializer(read_only=True)
    clazz = ClassSerializer(read_only=True)

重写你的视图创建方法,以查找自由形式属性上的相关类。另外,请查看DRF如何使用mixin实现此功能。如果选择这条路线,还必须重写update方法以正确处理这些内容,并考虑到PATCH(部分更新)支持,而不仅仅是PUT(更新)。

def create(self, request):
    # Look up objects by arbitrary attributes.
    # You can check here if your students are participating
    # the classes and have taken the subjects they sign up for.
    subject = get_object_or_404(Subject, title=request.data.get('subject'))
    clazz = get_object_or_404(
        Class, 
        number=request.data.get('clazz_number')
        letter=request.data.get('clazz_letter')
    )

    serializer = self.get_serializer(data=request.data)
    serializer.is_valid(raise_exception=True)
    serializer.save(clazz=clazz, subject=subject)
    headers = self.get_success_headers(serializer.data)

    return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)

选项二:为读和写专门定制序列化器并使用主键;这是惯用的方法:

首先定义一个默认的ModelSerializer,你希望在正常操作(POST、PUT、PATCH)中使用:

class ExamSerializer(serializers.ModelSerializer)
    class Meta:
        model = Exam
        fields = ('id', 'subject', 'clazz', 'topic', 'date', 'details')

接着,覆盖需要的字段以您想要为数据阅读(GET)提供的表示形式:

class ExamReadSerializer(ExamSerializer):
     subject = SubjectSerializer(read_only=True)
     clazz = ClassSerializer(read_only=True)

然后为您的ViewSet指定不同操作所需使用的序列化器。在这里,我们针对读取操作返回嵌套的Subject和Class数据,但仅对更新操作使用它们的主键(更简单):

class ExamViewSet(viewsets.ModelViewSet):
     queryset = Exam.objects.all()

     def get_serializer_class(self):
         # Define your HTTP method-to-serializer mapping freely.
         # This also works with CoreAPI and Swagger documentation,
         # which produces clean and readable API documentation,
         # so I have chosen to believe this is the way the
         # Django REST Framework author intended things to work:
         if self.request.method in ['GET']:
             # Since the ReadSerializer does nested lookups
             # in multiple tables, only use it when necessary
             return ExamReadSerializer
         return ExamSerializer

正如您所见,选项2似乎相对较少复杂且容易出错,除了DRF(get_serializer_class实现)的3行手写代码外,只需让框架逻辑为您找出对象的表示形式以及创建和更新即可。
我看到过许多其他方法,但到目前为止,这些方法对我来说产生的维护代码最少,并以干净的方式利用DRF的设计。

3
你好,来自2017年夏天的朋友。感谢你提供了详细的解释。关于你提出的“惯用语”解决方案存在一个问题:非GET查询后API返回的结果不包含嵌套对象。是否有办法在同一个HTTP查询中使用不同的序列化程序进行读写,以避免这种情况? - Grigoriy Beziuk
嗨@gbezyuk!您可以覆盖create方法并使用不同的序列化程序返回数据,而不是用于输入的序列化程序,但这将再次需要专门化 :) 您可以使用mixin在多个类中专门化行为,并编写自定义get_serializer_class实现,支持在参数中指定方法或目的的默认参数。确保遵守Liskov替换原则来专门化实现,并与基类兼容。 - Aleksi Häkli
嗨@AleksiHäkli,我的问题是我想在创建后返回嵌套的序列化数据。目前,例如,它只返回像{...,“m2m_field”:[100, 201],...}这样的东西,而我希望{...,“m2m_field”:[{'id':100,'foo':'bar',...},{'id':201,...}],...}。有适当的方法吗?谢谢 - achilles
嘿@achilles!正如之前的评论所述,您将不得不覆盖您正在使用的视图或视图集的方法。创建必须使用不同的序列化程序来处理从客户端到服务器的写入数据,并为将数据返回给客户端使用另一个序列化程序。您将不得不专门化例如create方法或get_serializer_class方法。 - Aleksi Häkli
get_serializer_class 中,我认为你应该使用 self.action 而不是 self.request.method。这里简要介绍了操作 https://www.django-rest-framework.org/api-guide/viewsets/#introspecting-viewset-actions - rvernica

17

如果不想创建额外的类,可以采用更简单的方法来进行序列化:

class ExamSerializer(serializers.ModelSerializer):
    class Meta:
        model = Exam
        fields = ('id', 'subject', 'clazz', 'topic', 'date', 'details')

    def to_representation(self, instance):
        data = super().to_representation(instance)
        data['subject'] = SubjectSerializer(
            Subject.objects.get(pk=data['subject'])).data
        data['clazz'] = ClassSerializer(
            Class.objects.get(pk=data['clazz'])).data
        return data

3
为了解决你的问题,你可以使用这个包 drf-rw-serializers
你只需要使用两个序列化器(一个用于读取,一个用于写入):

serializers.py

最初的回答:使用 drf-rw-serializers 包,使用两个序列化器来解决问题。
class ClassSerializer(serializers.ModelSerializer):
    class Meta:
        model = Class
        fields = ('number', 'letter')


class SubjectSerializer(serializers.ModelSerializer):
    class Meta:
        model = Subject
        fields = ('title',)


class ExamSerializer(serializers.ModelSerializer):
    subject = SubjectSerializer()
    clazz = ClassSerializer()

    class Meta:
        model = Exam
        fields = ('id', 'subject', 'clazz', 'topic', 'date', 'details')

class WriteExamSerializer(serializers.ModelSerializer):
    subject = SubjectSerializer()
    clazz = ClassSerializer()

    class Meta:
        model = Exam
        fields = ('id', 'subject', 'clazz', 'topic', 'date', 'details')

    def create(self, validated_data):
        subject = validated_data.pop('subject', None)
        # logic to update subject
        clazz = validated_data.pop('clazz', None)
        # logic to update clazz
        return super().create(validated_data)

    def update(self, validated_data):
        subject = validated_data.pop('subject', None)
        # logic to update subject
        clazz = validated_data.pop('clazz', None)
        # logic to update clazz
        return super().update(validated_data)

api_views.py

from drf_rw_serializers import generics

from .models import Exam
from .serializers import WriteExamSerializer, ExamSerializer


class ExamListCreateView(generics.ListCreateAPIView):
    queryset = Exam.objects.all()
    write_serializer_class = WriteExamSerializer
    read_serializer_class = ReadExamSerializer

这个包正好符合我的需求。它根据Django是返回给请求者还是在内部修补对象来进行序列化。 - pedrovgp

1

在尝试将嵌套的JSON对象发布到DRF(Django Rest Framework)时,我遇到了相同的问题。

一旦正确设置编写嵌套序列化程序(请参阅可写嵌套序列化程序文档),您可以使用浏览器API进行测试,并在那里发布/放置数据。如果那样做有效,并且在发布/放置JSON对象时仍然出现嵌套模型的“此字段为必填项”错误,则可能需要设置请求的内容类型。

这个答案提供了我所需的解决方案,并在下面概述。

$.ajax ({
  // Other parameters e.g. url, type
  data: JSON.stringify(data),
  dataType: "json",
  contentType: "application/json; charset=utf-8",
});

我需要设置 "contentType" 并将我的 js 对象转换为字符串。

0

我认为SerializerMethodField更加简单。

它看起来像@validname的解决方案,但更易读。

class BlogSerializer(serializers.ModelSerializer):

    writer = serializers.SerializerMethodField()
    comments = serializers.SerializerMethodField()

    class Meta:
        model = Blog
        fields = '__all__'

    def get_comments(self, obj):
        return CommentSerializer(obj.comments.all(), many=True).data

    def get_writer(self, obj):
        return WriterSerializer(instance=obj.writer).data

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