Django REST Framework自定义字段验证


75

我正在尝试为模型创建自定义验证,以检查它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类(然后添加到模型)中,但是似乎在任何一个位置都没有调用它。

我也发现这个代码,可能是使用的位,但我不知道如何在我的方法-整合似乎是它的工作来验证一个模型属性,但我需要两个属性之间的检查。

我的模特:

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

Answers:


98

您应该使用对象范围内的验证(validate()),validate_date因为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("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, serializer):
        if attrs['start_date'] > attrs['end_date']:
            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端点(如果您只想更改日期之一,则会导致序列化器错误)。对于任何发现此问题的人,请确保从django.core.exceptions中添加```到模型文件中导入ValidationError`。
加比

jadelange @您的解决方案对我有用,可以进行表单验证,因此您应该得到我的支持。我遇到了另一个问题,该问题不适用于其余框架l。你能解释一下吗?
阿米尔

哦,我忘了一件事。如果我在模型中使用clean方法,则该方法不适用于休息电话
Amir

2
值得注意的是,从DRF 3.0开始,clean不再按此说明调用模型的方法了…… django-rest-framework.org/topics/3.0
David M.

2
@DavidM。-该URL现在无效。这是正确的... django-rest-framework.org/community/3.0-announcement
ExTexan

23

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

(因为doc说: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.")

2
您的文档链接现在无效。你能更新吗?谢谢!
eikonomega

2
链接的文档说:“在某些情况下,您确实确实需要在模型.clean()方法中保留验证逻辑,而不能将其分离到序列化器.validate()中。您可以通过显式实例化模型来做到这一点。同样,如果可能的话,您确实应该考虑将验证逻辑从模型方法中正确分离出来,但是以上内容在某些向后兼容的情况下或对于简单的迁移路径可能很有用。” 所以它不是说“这样做”。俗话说,您可以在模型中完成此操作,但不能,而是使用序列化程序。
MT0

22

关于这种情况,如果选择覆盖序列化器的validate()方法,则另一个答案可能很有用。

关于Django REST Framework中序列化程序验证顺序的答案,我必须说,该serializer.validate()方法在验证序列的末尾被调用。然而,现场的验证之前被调用,以serializer.to_internal_value(),养ValidationError在最后。

这意味着自定义验证错误不会与默认错误堆积在一起

我认为达​​到预期行为的最干净的方法是在序列化程序类中使用目标字段方法验证:

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

1
我同意这是最干净的方法-谢谢
亚伦·威廉姆斯

谢谢,但是仅在串行器中粘贴该方法无效。还有什么需要做的?
Pithikos

@Pithikos,好吧,它肯定对我和其他人有用。您有什么具体条件吗?
损坏的有机

5

如果有人在现场将其实现为基于类的验证器而苦苦挣扎...

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字段,然后可以使用设置end_date现场字段validators=[EndDateValidator('start_date')]


5

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

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

另外,我使它具有可继承性,因此我们可以重新实现验证功能并重新使用代码。

验证器

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

4

如果您更喜欢简单的解决方案,尤其是如果您不打算多次重复使用验证器,那么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类接受带有错误详细信息的对象。这样,我可以模拟字段级别验证的相同行为,将错误消息与字段本身联系在一起,而我仍然可以比较每个验证单独的日期之后的日期。

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


我将更新我的答案以使错误显示在end_date现场。我同意那会更好
jgadelange
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.