Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Django REST Framework custom fields validation

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:

  • built-in Django validators: none check for this

  • writing my own, like so:

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

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')
like image 201
Gabi Avatar asked Jul 07 '15 20:07

Gabi


People also ask

How does Django validate data in serializer?

Validation in Django REST framework serializers is handled a little differently to how validation works in Django's ModelForm class. With ModelForm the validation is performed partially on the form, and partially on the model instance. With REST framework the validation is performed entirely on the serializer class.

Why do we need Serializers in Django REST framework?

Serializers in Django REST Framework are responsible for converting objects into data types understandable by javascript and front-end frameworks. Serializers also provide deserialization, allowing parsed data to be converted back into complex types, after first validating the incoming data.

How do I validate an email in Django REST framework?

This can be easily done by automatically sending the verification email with an activation link. Such a link contains the unique token assigned to the user. After opening the activation link in the web browser the request is sent to the web application (Django Rest Framework).


3 Answers

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

As suggested by Michel Sabchuk you can add the validation error to the end_date field:

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

Another possibility is to create a validator. I created one based on the code for 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()]

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.")
like image 59
jgadelange Avatar answered Oct 11 '22 19:10

jgadelange


Another answer here might be useful, regarding the 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 the 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 the model, such as start_date in this case, you can get them (yet unvalidated, as a process is not complete) with:

# `None` here can be replaced with the field's default value
start_date = self.initial_data.get('start_date')
like image 39
Damaged Organic Avatar answered Oct 11 '22 19:10

Damaged Organic


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.")
like image 29
Amir Avatar answered Oct 11 '22 19:10

Amir