I am trying to create custom validation for a model, to check that its start_date
is before its end_date
and it is proving near impossible.
Stuff I've tried:
That bit of code I have added to the Serializer class (and then the model), but it does not seem to get called in either location.
I also found this bit of code that might be of use, but I don't know how to integrate in my method- it seems that it would work to validate one model attribute, but I need to check between two attributes.
My model:
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')
Fyi, all the other validations work!
My serializer:
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'
)
My view:
class MyModelList(generics.ListCreateAPIView):
permission_classes = (IsAdminUser,)
queryset = MyModel.objects.all()
serializer_class = MyModelSerializer
ordering = ('priority')
You should use an object wide validation (validate()
), since validate_date
will never be called since date
is not a field on the serializer. 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
Pre DRF 3.0 you could also add it to the clean function of a model, but this is not called anymore in 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.")
jgadelange's answer worked before django rest 3 probably. If any one using the django rest framework 3* version, I think this would be helpful for that folk. one should keep validation process in model level and clean method may be the one solution. But django rest framework announcement says here that, if someone wants to validate rest-call in model .clean method, he/she should override the serializer validate method and need to call the clean method form this serializer class by the following way
(because doc says : clean() method will not be called as part of serializer validation)
class MySerializer(serializers.ModelSerializer):
def validate(self, attrs):
instance = MyModel(**attrs)
instance.clean()
return attrs
and model
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.")
Another answer here might be useful, regarding situation if one chooses to override serializer's validate()
method.
Regarding answer on Order of Serializer Validation in Django REST Framework, I must say that serializer.validate()
method is called at the end of validation sequence. However, field's validators are called before that, in serializer.to_internal_value()
, raising ValidationError
at the end.
This means that custom validation errors do not stack with default ones.
In my opinion cleanest way to achieve desired behaviour is by using target field method validation in serializer class:
def validate_end_date(self, value):
# validation process...
return value
In case if you need another field value from model, such as start_date
in this case, you can get them (yet unvalidated, as process is not complete) with:
# `None` here can be replaced with field's default value
start_date = 'start_date' in self.initial_data
and self.initial_data['start_date'] or None
In case anyone struggling with implementing this as class-based validator on field...
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)
Assuming you have start_date
and end_date
fields in your serializer, you can then set in on end_date
field with validators=[EndDateValidator('start_date')]
.