Model Layer Duplicating Validation Occurring in the Serializer

663 views
Skip to first unread message

David Muller

unread,
Dec 13, 2014, 3:00:15 PM12/13/14
to django-res...@googlegroups.com
I have a question about duplicating validation logic at the model layer and serializer layer in order to enforce simple constraints on values in my database.

Say I have the following django model that includes a validator on the the duration field (duration should be >=5):

from django.db import models
from django.core.validators import MinValueValidator

class ExampleModel(models.Model):
    MINIMUM_DURATION = 5
    duration = models.IntegerField(validators=[MinValueValidator(MINIMUM_DURATION)], default=MINIMUM_DURATION)

Now, I declare a rest_framework 3.0 serializer for that model:

from rest_framework import serializers

class ExampleModelSerializer(serializers.ModelSerializer):
    class Meta:
        model = ExampleModel

ExampleModelSerializer will enforce that any given input for the duration field is, in fact, >= MINIMUM_DURATION.  However, as written above, the model itself will not enforce the minimum value for duration. Someone on the backend could save an ExampleModel instance with a duration equal to 1 without error.  I decide I dislike this behavior, and adjust my model so that the model enforces the validation on its own:

class ExampleModel(models.Model):
    MINIMUM_DURATION = 5
    duration = models.IntegerField(validators=[MinValueValidator(MINIMUM_DURATION)], default=MINIMUM_DURATION)

    def save(self, *args, **kwargs):
        self.full_clean()
        super(ExampleModel, self).save(*args, **kwargs)  

Now, as far as I'm aware, if I use ExampleModelSeriailzer to create a new ExampleModel instance (say, through a public API), I'll run the validation on the duration field twice: the serializer will run the validation on the input, and then full_clean() will duplicate the validation when the model is saved.

Rest framework specific question: is there some way I can get around duplicating these validation checks? 

High level question: should I not be enforcing MinValueValidation at the model level at all (i.e. should I allow the database to contain values that are less than MINIMUM_DURATION)?  I feel like it's a no brainer to enforce my simple constraint (of duration >=5) at the database level to keep my data in a "clean"/expected state, but the fact that I'm duplicating validation logic whenever I instantiate a model using the serializer makes me feel like I'm doing something wrong.

David Fischer

unread,
Dec 15, 2014, 8:53:39 AM12/15/14
to django-res...@googlegroups.com
Hello,

I have a similar issue I fixed by implementing this mixin and using an extra argument to save():

class ValidateOnSaveMixin(object):


   
def save(self, *args, **kwargs):
       
if kwargs.pop('validate', True):
           
self.full_clean()
       
super(ValidateOnSaveMixin, self).save(*args, **kwargs)

I also implemented the business logic has methods of my managers:

def create(self, foo, validate=True):
    instance
= self.model(foo=foo)
    instance
.save(validate=validate)
   
return instance

My serializers will use these business logic methods with (..., validate=False) meaning the validations will run once at the serialization layer.

Hope this helps.

Tom Christie

unread,
Dec 15, 2014, 11:49:43 AM12/15/14
to django-res...@googlegroups.com
> is there some way I can get around duplicating these validation checks? 

Keep in mind that both `ModelForm` and 2.x serializers will always end up running field validations twice (once on the serializer/form and once on the model), so I wouldn't get too hung up on not duplicating the validation checks if you're running them on `save()`.

Running `.full_clean` on save with `ModelForm` or 2.x would actually mean you'd be running the checks 3 times - you're already doing better than that.

High level question: should I not be enforcing MinValueValidation at the model level at all

It's very valid to enforce the constraints at a model level, but note that if you're enforcing them at the point of save then these are *unchecked* validation failures (ie your application should be raising a 500 if you get to this point).

If you're running with the same style of encapsulation that I advocate here... http://www.dabapps.com/blog/django-models-and-encapsulation/

then `.save()` isn't something you'd be calling directly in any case, and your model level checks will be on the individual methods that present the allowable state changes, and failures would be best as assertion errors (since it's a hard application failure, not a checked validation failure)

Note that the new serializers also tie in nicely with the encapsulation style I'd recommend as they don't partially reconstruct a model instance and then save it later, but instead perform all validation first, and then call the model manager to create the instance. That's the layer at which you can assert correctness of values.

I also talk a bunch about this here: https://www.youtube.com/watch?v=3cSsbe-tA0E which might be helpful?

David Muller

unread,
Dec 15, 2014, 3:17:16 PM12/15/14
to django-res...@googlegroups.com
Thanks for your suggestion David, and your insights Tom!

@Tom -- I have read your Fat Models and Thin Views blog post, it's a great read.  

Noting that I ought not to worry about running validation twice, would the following update to the above models would fit into the "fat models and thin views" paradigm?
class ExampleModelManager(models.Manager):
    def create(self, *args, **kwargs):
        example_model = ExampleModel(*args, **kwargs)
        # run Django validators (in case we aren't calling `create` from a serializer)
        example_model.clean_fields()
        example_model.save()

class ExampleModel(models.Model):
    MINIMUM_DURATION = 5
    duration = models.IntegerField(validators=[MinValueValidator(MINIMUM_DURATION)], default=
MINIMUM_DURATION)

    objects = ExampleModelManager()

    def update(self, **kwargs):
        allowed_attributes = {'duration',}
        for name, value in kwargs.items():
            assert name in allowed_attributes
            setattr(self, name, value)
        # run Django validators (in case we aren't calling `update` from a serializer)
        self.clean_fields()
        self.save()
        return self
As far as I'm aware, the above code will throw a Django ValidationError to backend code that tries to `create()` or `update()` an instance with a `duration` < MINIMUM_DURATION.  DRF serializer's will raise an appropriate rest_framework ValidationError given a `duration` < MINIMUM_DURATION.  This does neglect raising assertion errors/ "hard application failures" to backend code that gives a `duration` <  MINIMUM_DURATION, but to me it seems that backend code should get the same sort of ValidationError that an API consumer would get if they try to input a `duration` <  MINIMUM_DURATION.
Reply all
Reply to author
Forward
0 new messages