Django REST Framework 自定义字段验证

Posted

技术标签:

【中文标题】Django REST Framework 自定义字段验证【英文标题】:Django REST Framework custom fields validation 【发布时间】:2015-09-25 12:32:20 【问题描述】:

我正在尝试为模型创建自定义验证,以检查其 start_date 是否在其 end_date 之前,这几乎是不可能的。

我尝试过的东西:

内置 Django 验证器:无此检查

自己写,像这样:

def validate_date(self):
   if self.start_date < self.end_date:
        raise serializers.ValidationError("End date must be after start date.")

我已添加到 Serializer 类(然后是模型)中的那段代码,但它似乎没有在任一位置被调用。

我还发现了this 可能有用的代码,但我不知道如何集成到我的方法中——它似乎可以验证一个模型属性,但我需要检查两个属性。

我的模特:

class MyModel(models.Model):

    created = models.DateTimeField(auto_now_add=True)
    relation_model = models.ForeignKey(RelationModel, related_name="mymodels")
    priority = models.IntegerField(
        validators = [validators.MinValueValidator(0), validators.MaxValueValidator(100)])
    start_date = models.DateField()
end_date = models.DateField()

    @property
    def is_active(self):
        today = datetime.date.today()
        return (today >= self.start_date) and (today <= self.end_date)

    def __unicode__(self):
        ...

    class Meta:
        unique_together = ('relation_model', 'priority', 'start_date', 'end_date')

仅供参考,所有其他验证都有效!

我的序列化器:

class MyModelSerializer(serializers.ModelSerializer):

    relation_model = RelationModelSerializer
    is_active = serializers.Field(source='is_active')

    def validate_date(self):
        if self.start_date > self.end_date:
            raise serializers.ValidationError("End date must be after start date.")   

    class Meta:
        model = MyModel
        fields = (
            'id', 'relation_model', 'priority', 'start_date', 'end_date', 'is_active'
        )

我的看法:

class MyModelList(generics.ListCreateAPIView):
    permission_classes = (IsAdminUser,)
    queryset = MyModel.objects.all()
    serializer_class = MyModelSerializer
    ordering = ('priority')

【问题讨论】:

【参考方案1】:

您应该使用对象范围的验证 (validate()),因为 validate_date 永远不会被调用,因为 date 不是序列化程序上的字段。 From the documentation:

class MySerializer(serializers.ModelSerializer):
    def validate(self, data):
        """
        Check that the start is before the stop.
        """
        if data['start_date'] > data['end_date']:
            raise serializers.ValidationError("finish must occur after start")
        return data

按照 Michel Sabchuk 的建议,您可以将验证错误添加到 end_date 字段:

class MySerializer(serializers.ModelSerializer):
    def validate(self, data):
        """
        Check that the start is before the stop.
        """
        if data['start_date'] > data['end_date']:
            raise serializers.ValidationError("end_date": "finish must occur after start")
        return data

另一种可能性是创建一个验证器。我根据UniqueTogetherValidator的代码创建了一个:

from rest_framework.utils.representation import smart_repr

class DateBeforeValidator:
    """
    Validator for checking if a start date is before an end date field.
    Implementation based on `UniqueTogetherValidator` of Django Rest Framework.
    """
    message = _('start_date_field should be before end_date_field.')

    def __init__(self, start_date_field="start_date", end_date_field="end_date", message=None):
        self.start_date_field = start_date_field
        self.end_date_field = end_date_field
        self.message = message or self.message

    def __call__(self, attrs):
        if attrs[self.start_date_field] > attrs[self.end_date_field]:
            message = self.message.format(
                start_date_field=self.start_date_field,
                end_date_field=self.end_date_field,
            )
            # Replace the following line with
            #   raise serializers.ValidationError(
            #       self.end_date_field: message,
            #       code='date_before',
            #   )
            # if you want to raise the error on the field level
            raise serializers.ValidationError(message, code='date_before')

    def __repr__(self):
        return '<%s(start_date_field=%s, end_date_field=%s)>' % (
            self.__class__.__name__,
            smart_repr(self.start_date_field),
            smart_repr(self.end_date_field)
        )


class MySerializer(serializers.ModelSerializer):
    class Meta:
        # If your start/end date fields have another name give them as kwargs tot the
        # validator:
        #   DateBeforeValidator(
        #       start_date_field="my_start_date", 
        #       end_date_field="my_end_date",
        #   )
        validators = [DateBeforeValidator()]

在 DRF 3.0 之前,您还可以将其添加到模型的 clean 函数中,但在 DRF 3.0 中不再调用它。

class MyModel(models.Model):
    start_date = models.DateField()
    end_date = models.DateField()
    def clean(self):
        if self.end_date < self.start_date:
            raise ValidationError("End date must be after start date.")

【讨论】:

谢谢,解决了我的问题!我决定将其添加到模型类中,因为将其添加到序列化程序会破坏 Edit 端点(如果您只想更改其中一个日期,则会导致序列化程序错误)。对于任何发现此帖子并提出相同问题的人,请确保将 ``` from django.core.exceptions import ValidationError ``` 添加到您的模型文件中。 jadelange@ 你的解决方案对我来说是表单验证的工作,所以你应该得到我的支持。我遇到了另一个问题,这不适用于休息框架 l。你能解释一下吗? 哦,我忘了提一件事。如果我在模型中使用 clean 方法,那么它不适用于休息调用 值得注意的是,从 DRF 3.0 开始,模型的 clean 方法将不再被调用,如此处所述...django-rest-framework.org/topics/3.0-announcement/… @David M. - 该网址现在无效。这是正确的...django-rest-framework.org/community/3.0-announcement【参考方案2】:

如果您喜欢更简单的解决方案,特别是如果您不打算多次重用验证器,jgadelange 和 Damaged Organic 的解决方案非常有趣,但我建议改进:我会使用对象级验证器,提高带有字段验证错误的字典:

def validate(self, data):
    ...
    if data["start_date"] > data["end_date"]:
        raise serializers.ValidationError(
            "end_date": "End date must be after start date."
        )
    ...

我正在利用 ValidationError class accepts an object with the error details.这样,我可以模拟字段级别验证的相同行为,将错误消息与字段本身联系起来,同时我仍然可以比较每个单独验证后的日期。

这对于确保您没有与在比较之前需要转换的不干净的开始日期进行比较很重要(就像您使用 self.initial_data 时所做的那样)。

【讨论】:

我将更新我的答案以在 end_date 字段上显示错误。我同意这样会更好【参考方案3】:

如果选择覆盖序列化程序的 validate() 方法,这里的另一个答案可能有用。

关于Order of Serializer Validation in Django REST Framework 的回答,我必须说serializer.validate() 方法是在验证序列结束时调用的。但是,在此之前会调用字段的验证器,在 serializer.to_internal_value() 中,最后提升 ValidationError

这意味着自定义验证错误不会与默认错误叠加

在我看来,实现所需行为的最简洁方法是在序列化程序类中使用 target field method 验证:

def validate_end_date(self, value):
    # validation process...
    return value

如果您需要模型中的另一个字段值,例如在这种情况下为start_date,您可以通过以下方式获取它们(但未验证,因为过程未完成):

# `None` here can be replaced with the field's default value
start_date = self.initial_data.get('start_date')

【讨论】:

我同意这是最干净的方法 - 谢谢 谢谢,但仅在序列化程序中粘贴该方法是行不通的。还需要做什么? @Pithikos,好吧,它肯定对我和其他人有用。你有什么具体条件吗? 有没有办法从目标字段方法访问序列化器的其他数据? @Vedmant,我记得 self 参数是对 Serializer 实例的引用。【参考方案4】:

我将扩展 Konrad 的答案。我喜欢它,因为它非常明确,而且当我们使用它们时,你正在调用其他字段的验证。所以它更安全,可能会是多余的(一些验证会被调用两次)

首先要注意的是,如果我们这样实现,当我们运行 run_validator 时,只会出现在 validators 变量中设置的验证。因此,如果我们使用 validate_ 方法验证一个字段,它将不会运行。

另外,我让它可继承,所以我们可以重新实现验证功能并重新整理代码。

validators.py

from rest_framework.serializers import ValidationError

class OtherFieldValidator:

    #### This part is the same for all validators ####

    def __init__(self, other_field):
        self.other_field = other_field # name of parameter

    def set_context(self, serializer_field):
        self.serializer_field = serializer_field # name of field where validator is defined

    def make_validation(self,field, other_field):
        pass

    def __call__(self, value):
        field = value
        serializer = self.serializer_field.parent # serializer of model
        raw_other_field = serializer.initial_data[self.other_field] # data del otro campo

        try:
            other_field = serializer.fields[self.other_field].run_validation(raw_other_field)
        except ValidationError:
            return # if date_start is incorrect we will omit validating range

    #### Here is the only part that changes ####

        self.make_validation(field,other_field)

class EndDateValidator(OtherFieldValidator):

    def make_validation(self,field, other_field):
        date_end = field
        date_start = other_field
        if date_start and date_end and date_end < date_start:
            raise ValidationError('date cannot be')

所以序列化器会是这样的:serializers.py

# Other imports
from .validators import EndDateValidator

 def myfoo(value):                                                        
     raise ValidationError("start date error")                             

 class MyModelSerializer(serializers.ModelSerializer):                                        
     class Meta:                                                          
         model = MyModel                                                      
         fields = '__all__'                                                                                       
         extra_kwargs =                                                  
             'date_end': 'validators': [EndDateValidator('date_start')],
             'date_start': 'validators': [myfoo],                       
                                                                         

【讨论】:

【参考方案5】:

jgadelange 的答案可能在 django rest 3 之前有效。如果有人使用 django rest framework 3* 版本,我认为这对那些人会有所帮助。应该将验证过程保持在模型级别,而干净的方法可能是一种解决方案。但是django rest框架公告说here,如果有人想验证模型.clean方法中的rest-call,他/她应该重写序列化器验证方法,并且需要通过以下方式从这个序列化器类调用clean方法

(因为文档说:clean() 方法不会作为序列化程序验证的一部分被调用)

class MySerializer(serializers.ModelSerializer):

   def validate(self, attrs):
     instance = MyModel(**attrs)
     instance.clean()
     return attrs

和模型

class MyModel(models.Model):
    start_date = models.DateField()
    end_date = models.DateField()

    def clean(self):
        if self.end_date < self.start_date:
            raise ValidationError("End date must be after start date.")

【讨论】:

您的文档链接现在无效。你能更新一下吗?谢谢! 链接的文档说“在某些情况下,您确实需要在模型 .clean() 方法中保留验证逻辑,而不能将其分离到序列化程序 .validate() 中。您可以通过在 .validate() 方法中显式实例化模型实例来做到这一点。同样,如果可能的话,您确实应该考虑将验证逻辑从模型方法中正确分离出来,但上述方法在某些向后兼容的情况下可能很有用,或者一个简单的迁移路径。”所以它不是说“这样做”;它说你可以在模型中做到这一点,但不要,改用序列化器。 这不是对序列化程序的正确验证。【参考方案6】:

如果有人努力将其作为基于类的验证器在字段上实现......

from rest_framework.serializers import ValidationError

class EndDateValidator:
    def __init__(self, start_date_field):
        self.start_date_field = start_date_field

    def set_context(self, serializer_field):
        self.serializer_field = serializer_field

    def __call__(self, value):
        end_date = value
        serializer = self.serializer_field.parent
        raw_start_date = serializer.initial_data[self.start_date_field]

        try:
            start_date = serializer.fields[self.start_date_field].run_validation(raw_start_date)
        except ValidationError:
            return  # if start_date is incorrect we will omit validating range

        if start_date and end_date and end_date < start_date:
            raise ValidationError(' cannot be less than '.format(self.serializer_field.field_name, self.start_date_field)

假设您的序列化程序中有start_dateend_date 字段,然后您可以使用validators=[EndDateValidator('start_date')] 设置end_date 字段。

【讨论】:

以上是关于Django REST Framework 自定义字段验证的主要内容,如果未能解决你的问题,请参考以下文章

python 使用Django Rest Framework在Django中自定义用户实现

Django rest framework 之 DictField、ListField、自定义字段

django-rest-framework 自定义视图集检索多个查找参数

Django Rest Framework 自定义身份验证

Django REST Framework 创建自定义用户

与 Django Rest Framework 的非用户连接的自定义身份验证