96

I'm not sure how to properly raise a validation error in a model's save method and send back a clear message to the user.

Basically I want to know how each part of the "if" should end, the one where I want to raise the error and the one where it actually saves:

def save(self, *args, **kwargs):
    if not good_enough_to_be_saved:
        raise ValidationError
    else:
        super(Model, self).save(*args, **kwargs)

Then I want to know what to do to send a validation error that says exactly to the user what's wrong just like the one Django automatically returns if for example a value is not unique. I'm using a (ModelForm) and tune everything from the model.

1
  • use clean() method
    – Luv33preet
    Aug 27, 2018 at 12:07

7 Answers 7

86

Most Django views e.g. the Django admin will not be able to handle a validation error in the save method, so your users will get 500 errors.

You should do validation on the model form, on the model’s clean method, or by adding validators to the model’s fields. Then call save() only if the model form data is valid, in which case it is 'good enough to save'.

9
  • 3
    You're right I will move my validation into the form, it's way easier. I just liked the idea of having everything in the model.
    – Bastian
    Jan 8, 2012 at 7:02
  • 20
    @bastian, I also liked to having everything in the model. It's easy to forget a business rule when you write a new form, but not if business rules are in the model. For this reason I have moved validations from forms to model as I explain in my post. I'm open to learn about new methods to do this in a more elegant way if it exists. In any case I avoid to write validation code on forms. Jan 8, 2012 at 14:31
  • 10
    It's fine to put validation in your model by using validators or writing a clean() method. All I was saying is that the save() method isn't the correct place. Have a look at the docs on validating objects.
    – Alasdair
    Jan 8, 2012 at 17:04
  • 7
    I don't understand why validation should only be done in the form side and not the model save side. Like there aren't other ways of creating an object. What if you want to instantiate and create an object withouth using a form and still want to guarantee a certain state?
    – dabadaba
    Jan 9, 2019 at 11:36
  • 4
    @dabadaba you can put the validation in the model's clean method, I only said not to put it in the model's save() method. If you put the validation in the save() method, then you'll get 500 errors from most views because they will not handle the ValidationError. Note that putting the validation in the save() method is not an absolute guarantee - you could still write Model.objects.filter(...).update(...) or manual SQL that results in invalid data being saved.
    – Alasdair
    Jan 9, 2019 at 12:10
34

Bastian, I explain to you my code templating, I hope that helps to you:

Since django 1.2 it is able to write validation code on model. When we work with modelforms, instance.full_clean() is called on form validation.

In each model I overwrite clean() method with a custom function (this method is automatically called from full_clean() on modelform validation ):

from django.db import models
 
class Issue(models.Model):
    ....
    def clean(self): 
        rules.Issue_clean(self)  #<-- custom function invocation

from issues import rules
rules.connect()

Then in rules.py file I write bussiness rules. Also I connect pre_save() to my custom function to prevent save a model with wrong state:

from issues.models import Issue

def connect():    
    from django.db.models.signals import post_save, pre_save, pre_delete
    #issues 
    pre_save.connect(Issue_pre_save, sender = Incidencia ) 
    post_save.connect(Issue_post_save, sender = Incidencia )
    pre_delete.connect(Issue_pre_delete, sender= Incidencia) 

def Incidencia_clean( instance ):    #<-- custom function 
    import datetime as dt    
    errors = {}

    #dia i hora sempre informats     
    if not instance.dia_incidencia:   #<-- business rules
        errors.setdefault('dia_incidencia',[]).append(u'Data missing: ...')
        
    #dia i hora sempre informats     
    if not  instance.franja_incidencia: 
        errors.setdefault('franja_incidencia',[]).append(u'Falten Dades: ...')
 
    #Només es poden posar incidències més ennlà de 7 dies 
    if instance.dia_incidencia < ( dt.date.today() + dt.timedelta( days = -7) ): 
        errors.setdefault('dia_incidencia 1',[]).append(u'''blah blah error desc)''')
 
    #No incidències al futur. 
    if instance.getDate() > datetime.now(): 
        errors.setdefault('dia_incidencia 2',[]).append(u'''Encara no pots ....''') 
    ... 

    if len( errors ) > 0: 
        raise ValidationError(errors)  #<-- raising errors

def Issue_pre_save(sender, instance, **kwargs): 
    instance.clean()     #<-- custom function invocation

Then, modelform calls model's clean method and my custon function check for a right state or raise a error that is handled by model form.

In order to show errors on form, you should include this on form template:

{% if form.non_field_errors %}
      {% for error in form.non_field_errors %}
        {{error}}
      {% endfor %}
{% endif %}  

The reason is that model validation erros ara binded to non_field_errors error dictionary entry.

When you save or delete a model out of a form you should remember that a error may be raised:

try:
    issue.delete()
except ValidationError, e:
    import itertools
    errors = list( itertools.chain( *e.message_dict.values() ) )

Also, you can add errors to a form dictionary on no modelforms:

    try:
        #provoco els errors per mostrar-los igualment al formulari.
        issue.clean()
    except ValidationError, e:
        form._errors = {}
        for _, v in e.message_dict.items():
            form._errors.setdefault(NON_FIELD_ERRORS, []).extend(  v  )

Remember that this code is not execute on save() method: Note that full_clean() will not be called automatically when you call your model’s save() method, nor as a result of ModelForm validation. Then, you can add errors to a form dictionary on no modelforms:

    try:
        #provoco els errors per mostrar-los igualment al formulari.
        issue.clean()
    except ValidationError, e:
        form._errors = {}
        for _, v in e.message_dict.items():
            form._errors.setdefault(NON_FIELD_ERRORS, []).extend(  v  )
3
  • 2
    Moltes gràcies for your lengthy explanation. I was looking for something automatic, Djangoish. Your example might interest me for other situations but the one I am writing now is just a 1 line validation so I won't implement the whole thing here.
    – Bastian
    Jan 8, 2012 at 7:01
  • you can always override the clean method with the 1 line validation... Aug 28, 2017 at 12:35
  • hmm.. this doesn't work for me. I'm using a pop-up form and the Exception ends up displaying instead of a validation error. I should point out that because I have a form that works with two models I am extending forms.Form instead of models.Form
    – geoidesic
    Apr 5, 2018 at 14:49
9

I think this is more clear way to do that for Django 1.2+

In forms it will be raised as non_field_error, in other cases, like DRF you have to check this case manual, because it will be 500 error.

class BaseModelExt(models.Model):
    is_cleaned = False

    def clean(self):
        # check validation rules here

        self.is_cleaned = True

    def save(self, *args, **kwargs):
        if not self.is_cleaned:
            self.clean()

        super().save(*args, **kwargs)
2
  • 2
    This seems very simple and effective to me, whenever you need to validate object created programmatically, that is: no form submission is involved in the process. Thank you Apr 24, 2020 at 9:04
  • You forgot to reset (is_cleaned = False) after saving, and you'd probably also want to this after BaseModelExt.refresh_from_db().
    – Joren
    Dec 19, 2022 at 1:43
2

In the Django documentation they raise the ValueError in the .save method, it's maybe useful for you.

https://docs.djangoproject.com/en/3.1/ref/models/instances/

0

Edit: This answer assumes that you have a scenario that does not allow you to edit the currently implemented User class, because you are not starting a project from scratch, the current implementation does not already use a custom User class, and you instead have to figure out how to accomplish this task by modifying Django's built in User model behavior.

You can just stick a clean method to your model most of the time, but you don't have that option necessarily with the built in auth.User model. This solution will allow you to create a clean method for the auth.User model in such a way that ValidationErrors will propagate to forms where the clean method is called (including admin forms).

The below example raises an error if someone attempts to create or edit an auth.User instance to have the same email address as an existing auth.User instance. Disclaimer, if you are exposing a registration form to new users, you do not want your validation error to call out usernames as mine does below.

from django.contrib.auth.models import User
from django.forms import ValidationError as FormValidationError

def clean_user_email(self):
    instance = self
    super(User, self).clean()
    if instance.email:
        if User.objects.filter(id=instance.id, email=instance.email).exists():
            pass  # email was not modified
        elif User.objects.filter(email=instance.email).exists():
            other_users = [*User.objects.filter(email=instance.email).values_list('username', flat=True)]
            raise FormValidationError(f'At least one other user already has this email address: '
                                      f'{", ".join(other_users)}'
                                      , code='invalid')

# assign the above function to the User.clean method
User.add_to_class("clean", clean_user_email)

I have this at the bottom of my_app.models but I am sure it would work as long as you stick it somewhere that is loaded before the form in question.

6
  • 1
    If you don't like my answer you should explain why.
    – DragonBobZ
    Mar 19, 2019 at 22:53
  • I didn't downvote, but I'm guessing the downvote is because you're answering a question from 2012 with something that [A] (though interesting) is not an answer to the question asked, [B] doesn't call any existing User.clean(), and [C] uses monkey-patching instead of inheriting from AbstractUser and implementing clean() on your own class...
    – thebjorn
    Jun 2, 2019 at 11:57
  • 1
    It's not my own class. The User model is defined by Django. I had to do this monkey patch to modify methods on Django's built in user model because after you have started a project and it is in production without an AbstractUser custom User model implementation, it is basically impossible to successfully retro-fit your own User model. Notice that the first two sentences of my answer explicitly address your stated concern.
    – DragonBobZ
    Jun 3, 2019 at 17:07
  • 1
    Additionally, I "answered a question from 2012" with the answer that worked for my situation because when I looked to solutions for my particular problem, this is the question that came up in 2018. So lets say someone like me comes along and has this problem. Well, there's a possible solution, which took me a non-negligible amount of time to come up with, and which could save someone a near-equivalent amount of time. To my understanding, Stack Overflow is intended to be a useful aggregation of solutions. Having potential edge cases covered is very much a part of that.
    – DragonBobZ
    Jun 3, 2019 at 17:25
  • 1
    As I said, I didn't downvote, but all your justifications for why this is a cool solution (it is) brings you farther from an answer to this question. Instead of appending your solution to your own problem onto an old semi-related question you found when searching for solutions to your problem (and getting downvoted), might I suggest that you create your own new question? It's perfectly ok to answer your own question, so if you have hard-won experience to share you can self-answer (and potentially get up-votes for both the question and the answer).
    – thebjorn
    Jun 3, 2019 at 17:38
0

If you want to do validation on the model, you can use the clean() or clean_fields methods on the model.

EDIT: These are called by django prior to executing save() and Validation errors are handled in a user friendly way is incorrect, thanks for picking that up @Brad.

These clean and clean_fields methods are called by Django's Form validators prior to saving a model (e.g. in django admin, in which case your validation error is handled nicely), but are not called on save(), automatically by DRF serialisers or if you're using custom views, in which case you have to ensure they're called (or validate another way, e.g. by putting the logic into your serializer's validations).

Worth highlighting: If you put custom validation logic directly into save() and raise a ValidationError from there, that doesn't play nicely with forms (e.g. breaks the admin with a 500 error), which makes things a real pain if you want both django-admin and DRF to work well together... you basically have to either duplicate the validation logic in both the serializers and the clean* methods or find some awkward way doing validation that can be shared with both.

Django docs on ValidationErrors here..

2
  • 2
    "these are called by django prior to executingsave()". No, they're not. Jan 16, 2022 at 19:08
  • Well spotted, Brad. I'd answered quickly and had forgotten the validation is done at form level, not save() level.
    – thclark
    Jan 17, 2022 at 12:02
-2
def clean(self):
    raise ValidationError("Validation Error")

def save(self, *args, **kwargs):
    if some condition:
        #do something here
    else:
        self.full_clean()
    super(ClassName, self).save(*args, **kwargs)
2
  • 3
    Posting code is not enough, you should provide some explanation.
    – user1143634
    Dec 1, 2017 at 11:58
  • 1
    you can call full_clean() method in save function, this works fine in Django==1.11, i am not sure about the older version. Dec 1, 2017 at 12:35

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.