[Django]-Django DRY Model/Form/Serializer Validation

6👍

Just realized I never posted my solution back to this question. I ended up writing a model mixin to always run validation before saving; it’s a bit inconvenient as validation will technically be run twice in Django’s forms (i.e. in the admin panel), but it lets me guarantee that validation is run — regardless of what triggers a model save. I generally don’t use Django’s forms, so this doesn’t have much impact on my applications.

Here’s a quick snippet that does the trick:

class ValidatesOnSaveModelMixin:
    """ ValidatesOnSaveModelMixin
    A mixin that ensures valid model state prior to saving.
    """
    def save(self, **kwargs):
        self.full_clean()
        super(ValidatesOnSaveModelMixin, self).save(**kwargs)

Here is how you’d use it:

class ImportantModel(ValidatesOnSaveModelMixin, models.Model):
    """ Will always ensure its fields pass validation prior to saving. """

There is one important caveat: any of Django’s direct-to-database operations (i.e. ImportantModel.objects.update()) don’t call a model’s save() method, and therefore will not be validated. There’s not much to do about this, since these methods are really about optimizing performance by skipping a bunch of database calls — so just be aware of their impact if you use them.

4👍

I agree, the link between models/serializers/validation is broken.

The best DRY solution I’ve found is to keep validation in model, with validators specified on fields, then if needed, model level validation in clean() overridden.

Then in serializer, override validate and call the model clean() e.g. in MySerializer:

def validate(self, data):
    instance = FooModel(**data)
    instance.clean()
    return data

It’s not nice, but I prefer this to 2-level validation in serializer and model.

👤jmoz

1👍

Just wanted to add on SamuelMS’s answer.
In case you use F() expressions and similar. As explained here this will fail.

class ValidatesOnSaveModelMixin:
    """ ValidatesOnSaveModelMixin
    A mixin that ensures valid model state prior to saving.
    """
    def save(self, **kwargs):
        if 'clean_on_save_exclude' in kwargs:
             self.full_clean(exclude=kwargs.pop('clean_on_save_exclude', None)
        else:
             self.full_clean()
        super(ValidatesOnSaveModelMixin, self).save(**kwargs)

Then just use it the same way he explained.
And now when calling save, if you use query expressions can just call

instance.save(clean_on_save_exclude=['field_name'])

Just like you would exclude if you were calling full_clean and exclude the fields with query expressions.
See https://docs.djangoproject.com/en/2.2/ref/models/instances/#django.db.models.Model.full_clean

Leave a comment