Django REST框架 - 序列化可选字段

52

我有一个包含可选字段的对象。我已经按照以下方式定义了我的序列化器:

class ProductSerializer(serializers.Serializer):
    code = serializers.Field(source="Code")
    classification = serializers.CharField(source="Classification", required=False)

认为 required=False 可以绕过字段,如果它不存在的话。然而,文档中提到这会影响反序列化而不是序列化。
我遇到了以下错误:
'Product' object has no attribute 'Classification'

当我尝试访问序列化实例的.data时会发生什么?(这不意味着是反序列化引起了这个问题吗?)对于没有Classification的实例会出现这种情况。如果我从序列化器类中省略Classification,它就可以正常工作。如何正确地做到这一点?序列化具有可选字段的对象,也就是说。

这些字段被序列化为 None 是可以接受的吗?还是说根本不应该出现这个键? - Tom Christie
它们根本不存在,我正在使用suds调用一个具有可选字段的SOAP Web服务,响应对象表示返回的XML,在某些情况下不包括可选字段。 - Aziz Alfoudari
Tom,我刚意识到你的意思;理想情况下,我希望它们根本不存在,但是目前我可以接受“None”。 - Aziz Alfoudari
8个回答

33

下面描述的方法适用于我。 非常简单,容易上手并且对我有效。

使用的DRF版本为djangorestframework(3.1.0)

class test(serializers.Serializer):
  id= serializers.IntegerField()
  name=serializers.CharField(required=False,default='some_default_value')

更新: 这对我有用,我正在使用Django 4和DRF版本3.14.0。 "allow_blank"的想法不起作用。因为它仍然需要传递一个空字符串才能被视为有效并被接受。 - undefined

28

Django REST Framework 3.0+
现在支持动态字段,请查看http://www.django-rest-framework.org/api-guide/serializers/#dynamically-modifying-fields -- 这种方法定义序列化器中的所有字段,然后允许您选择性地删除不需要的字段。

或者您还可以像这样对模型序列化器进行操作,在序列化器init中混合 Meta.fields:

class ProductSerializer(serializers.ModelSerializer):
    class Meta:
        model = Product
        fields = ('code',)

    def __init__(self, *args, **kwargs):
        if SHOW_CLASSIFICATION: # add logic here for optional viewing
            self.Meta.fields = list(self.Meta.fields)
            self.Meta.fields.append('classification')
        super(ProductSerializer, self).__init__(*args, **kwargs)

如果这种方式不符合长期计划,你需要问一下Tom是否认为这是“正确的方式”。

Django REST Framework < 3.0
尝试像这样做:

class ProductSerializer(serializers.Serializer):
    ...
    classification = serializers.SerializerMethodField('get_classification')

    def get_classification(self, obj):
        return getattr(obj, 'classification', None)

多个序列化器

另一种方法是创建多个具有不同字段集的序列化器。其中一个序列化器继承自另一个序列化器并添加其他字段。然后,您可以使用get_serializer_class方法在视图中选择适当的序列化器。以下是我使用此方法调用不同的序列化器来呈现不同用户数据的实际示例,如果用户对象与请求用户相同。

def get_serializer_class(self):
    """ An authenticated user looking at their own user object gets more data """
    if self.get_object() == self.request.user:
        return SelfUserSerializer
    return UserSerializer

删除表现中的字段

另一种我在安全环境中使用过的方法是在to_representation方法中删除字段。定义一个方法,如下:

def remove_fields_from_representation(self, representation, remove_fields):
    """ Removes fields from representation of instance.  Call from
    .to_representation() to apply field-level security.
    * remove_fields: a list of fields to remove
    """
    for remove_field in remove_fields:
        try:
            representation.pop(remove_field)
        except KeyError:
            # Ignore missing key -- a child serializer could inherit a "to_representation" method
            # from its parent serializer that applies security to a field not present on
            # the child serializer.
            pass

然后在您的序列化器中调用该方法,如下:

def to_representation(self, instance):
    """ Apply field level security by removing fields for unauthorized users"""
    representation = super(ProductSerializer, self).to_representation(instance)
    if not permission_granted: # REPLACE WITH PERMISSION LOGIC
        remove_fields = ('classification', ) 
        self.remove_fields_from_representation(representation, remove_fields)
    return representation

这种方法直截了当且灵活,但代价是对有时不显示的字段进行序列化。 但那也许没有关系。


这通常是根据权限有条件地显示数据的任务,我想知道是否没有舒适的方式来处理它。 看起来你的第一种方法会起作用,但这有点像猴子补丁,而且每次创建新实例时,你的ProductSerializer.Meta.fields都会增加,你将失去它的最初状态。 - meteor
@meteor 我在最后添加了另一个示例,展示我如何解决这个安全问题。我不再推荐使用你提到的那个示例。 - Mark Chackerian
第一个示例不是非常接近文档中的建议吗?作为一个新手,我很困惑为什么这不是一个好方法? - Braden Holt
1
也许 - 这个答案是在很久以前写的,当时还没有标准的方法来做这件事 :-)。如果我是你,我会按照文档中的示例开始操作。不过,在某些特殊情况下,我曾经使用过这些技术中的每一种,所以它们仍然可能对某些人有用。 - Mark Chackerian

19

序列化器被有意地设计为使用一组固定的字段,因此您不能轻易地选择性地删除其中一个键。

您可以使用SerializerMethodField来返回字段值或None(如果该字段不存在),或者您可以根本不使用序列化器,直接编写视图返回响应。

REST框架3.0更新,可以在实例化的序列化器上修改serializer.fields。当需要动态的序列化器类时,我建议在自定义的Serializer.__init__()方法中修改字段。


10
考虑到该框架目前对我的项目非常友好,如果能够提供一个开箱即用的选项,让不存在的键默认为 None,那就太好了。 - Aziz Alfoudari
@TomChristie 嘿Tom,现在DRF 3已经发布了,你能否更新你的答案,推荐使用DRF 3的方法来完成这个任务?我之所以问是因为有几个人建议使用不同的DRF 3方法,我不确定哪种是最好的选择(请参考下面Mark的答案或者点击这里查看David的答案:https://dev59.com/K18d5IYBdhLWcg3wRAeE,这些都是使用DRF 3实现此功能的示例。你认为使用DRF 3的最佳方式是什么?) - SilentDev

6

序列化器的Charfield方法有一个属性allow_blank

默认情况下它设置为False。 将其设置为True,可以在“序列化”期间将字段标记为可选。

以下是您应该编写的代码

classification = serializers.CharField(source="Classification", allow_blank=True)

注意:`required` 属性用于反序列化。

3

DynamicSerializer 是 DRF 3 中的一个功能,允许动态指定哪些字段将用于序列化,哪些字段将被排除,以及可以选择哪些字段是必需的!

  1. 创建 Mixin
    class DynamicSerializerMixin:
        """
        A Serializer that takes an additional `fields` argument that
        controls which fields should be used.
        """

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

            # Instantiate the superclass normally
            super().__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)

                if isinstance(fields, dict):
                    for field, config in fields.items():
                        set_attrs(self.fields[field], config)

            if excluded_fields is not None:
                # Drop any fields that are not specified in the `fields` argument.
                for field_name in excluded_fields:
                    self.fields.pop(field_name)

            if required_fields is not None:
                for field_name in required_fields:
                    self.fields[field_name].required = True
  1. 通过继承DynamicSerializerMixin,来初始化/调整你的序列化器

class UserProfileSerializer(DynamicSerializerMixin, serializers.ModelSerializer):

    class Meta:
        model = User
        fields = (
            "id",
            'first_name', 'last_name'
            "email",
            "is_staff",
        )
  1. 使用它 :)
class RoleInvitationSerializer(serializers.ModelSerializer):
    invited_by = UserProfileSerializer(fields=['id', 'first_name', 'last_name'])

或者实时 API

    @action(detail=True, serializer_class=YourSerialzierClass)
    def teams_roles(self, request, pk=None):
        user = self.get_object()
        queryset = user.roles.all()
        serializer = self.get_serializer(queryset, many=True, excluded_fields=['user'])
        return Response(data=serializer.data)

1

在我的经验中,将序列化程序设置为以下方式是有效的:

classification = serializers.CharField(max_length=20, allow_blank=True, default=None)


1
为此,序列化程序具有partial参数。如果在初始化序列化程序时可以传递partial=True。如果您使用泛型或混合,则可以按以下方式覆盖get_serializer函数:
def get_serializer(self, *args, **kwargs):
    kwargs['partial'] = True
    return super(YOUR_CLASS, self).get_serializer(*args, **kwargs)

那样就可以解决问题。

注意: 这允许所有字段都是可选的,而不仅仅是特定的字段。如果你只想要特定的字段,你可以重写方法(即update)并添加存在验证来验证各个字段。


0

从“这是一个可怕的黑客,依赖于DRF和Django的特定实现细节,但它(至少现在)有效”的文件中,这是我用来在序列化器上的“创建”方法实现中包含一些额外调试数据的方法:

def create(self, validated_data)
    # Actual model instance creation happens here...
    self.fields["debug_info"] = serializers.DictField(read_only=True)
    my_model.debug_info = extra_data
    return my_model

这是一个临时的方法,让我可以使用浏览器可浏览的API展示在创建过程中从特定远程服务接收到的一些原始响应数据。将来,我倾向于保留这种能力,但不会默认返回更低级别的信息,而是将其隐藏在创建请求后面的“报告调试信息”标志中。


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