Django Custom Form Validation
It's good practice to not accept user-submitted data as-is. Form data needs to be validated to make sure the data is valid and meets any necessary conditions. In Django, this validation happens at two levels - the field level and the form level, both of which allow for custom validation to be added.
Django's Built-In Form Validation
Django helps us out by automatically validating the fields of a form. This means we can be sure the data submitted is of the type that the form field expects and, if it isn't, that an error will be raised.
If, for example, we expect the user to submit a date and instead they submit "octopus" then an error will be raised telling the user that "octopus" is not a valid date. Django can also provide more automatic validation depending on the form field. You can find what specific validations are available for each form field in the Django docs.
At the form level, Django can perform automatic checks for uniqueness between multiple fields. These checks depend on how we have setup our models and helps make sure users don't submit duplicate data if we don't want them to.
But what if you want to make sure that what a user submits meets some other condition that isn't automatically validated? For example, if we expected the user to submit a palindrome (a word or phase the same forwards and backwards), how could we check that it is one? Or, if we only wanted users to submit names of people who have first and last names that start with the same letter how could we check that?
Let's explore both of these scenarios.
Single Field Validation
We can check if a user-submitted word is a palindrome by doing extra validation at the field level.
First, let's look at our palindrome model which has only one field for our word.
#models.py from django.db import models class Palindrome(models.Model): word = models.CharField(max_length=250, blank=False) def __str__(self): return self.word
Next, we'll make a form for a user to be able to submit a palindrome.
#forms.py from django.forms import ModelForm from .models import Palindrome class PalindromeForm(ModelForm): class Meta: model=Palindrome fields = ['word']
We are inherting from ModelForm since we know we want to save the user input to the Palindrome model. At this point, the form only has Django's automatic form validation.
If we were to tie the form to a view that renders a template with the form, what would happen when the user submits a word in the form that isn't a palindrome?
It gets saved in the database.
That isn't what we want, but we haven't done anything to prevent it yet!
To check if a submitted word is a palindrome, we have to modify our PalindromeForm to include a test that checks if the word is the same forward as it is backwards. Let's do that now.
#forms.py from django.forms import ModelForm, ValidationError # Import ValidationError from .models import Palindrome class PalindromeForm(ModelForm): def clean_word(self): # Get the user submitted word from the cleaned_data dictionary data = self.cleaned_data["word"] # Check if the word is the same forward and backward if data != "".join(reversed(data)): # If not, raise an error raise ValidationError("The word is not a palindrome") # Return data even though it was not modified return data class Meta: model = Palindrome fields = ["word"]
We only need to perform extra validation at the field level to check if a word is a palindrome. To add this extra field valdiation, we define a method, clean_word, that operates only on the word field. This method will be called after Django's automatic validation checks which creates the cleaned_data dictionary for us. We first get the word from cleaned_data and store it as data.
Now we finally get to check if the word the user submitted is a palindrome by comparing it to the reversed version of itself. If the word is a palindrome, the check passes and the method returns the original data. If the word is not a palindrome, we raise a ValidationError with a customized error message. This message will be added to the form and displayed to the user when it is rerendered with errors due to the form validation failure.
If our form had multiple fields that we wanted to validate, we just need to add a clean_<fieldname> method for each field that we want to add custom validation for. We could have also included multiple validation checks in our clean_word method if, for example, we also wanted to make sure each palindrome submitted was more than four letters long.
Now let's look at our example of a form that has fields for first and last name and we want them both to begin with the same letter. In this case, since we need to perform a validation check that involves more than one field, our validation is performed at the form level.
Our model will have two CharFields of first_name and last_name.
#models.py from django.db import models class Person(models.Model): first_name = models.CharField(max_length=250, blank=False) last_name = models.CharField(max_length=250, blank=False)
Like the previous example, we will start with a basic form that has no custom validation.
#forms.py from django.forms import ModelForm from .models import Person class PersonForm(ModelForm): class Meta: model = Person fields = ["first_name", "last_name"]
Just like before, our form will allow any character string to be submitted for the first name and last name and will create and save a model instance with those values with no errors.
To add our first letter validation check, we override the clean method in the form.
#forms.py from django.forms import ModelForm, ValidationError # Import ValidationError from .models import Person class PersonForm(ModelForm): def clean(self): # Get the user submitted names from the cleaned_data dictionary cleaned_data = super().clean() first_name = cleaned_data.get("first_name") last_name = cleaned_data.get("last_name") # Check if the first letter of both names is the same if first_name.lower() != last_name.lower(): # If not, raise an error raise ValidationError("The first letters of the names do not match") return cleaned_data class Meta: model = Person fields = ["first_name", "last_name"]
This differs from the previous example where we created a method clean_<fieldname> because we are now looking at multiple fields instead of a single field. When overriding clean on a ModelForm we first call the clean method of the parent class to be able to have access to cleaned_data. Other form types may not return cleaned_data from the clean method on the parent class. In that case, we call super().clean() on its own with no assignment and access the cleaned data via self.cleaned_data. Once we have cleaned_data we can get the values of first_name and last_name. Then we can perform the actual validation check of whether the first and last name begin with the same letter. If they do, no error is raised and form processing continues. If they don't match, we raise a validation error that gets added to the form and displayed to the user.
Custom form validation is performed in different ways depending on whether the validation is needed at the field level for a single form field or at the form level for multiple fields.
To add validation for a single field a method of clean_<fieldname> is added to the form for the desired fieldname to be validated. This method should get the field data from the self.cleaned_data dictionary and return the field data whether is it modified or not. Conditional statements are added to the method to perform the desired validation and raise ValidationErrors if a validation check fails.
For validating the relationship between multiple fields, the clean method on the form is overridden. The field data is made available by a call to the parent class's clean method via super().clean() which returns the cleaned field data for a ModelForm. All field data from the form can now be accessed and the desired validation check performed to raise a ValidationError if necessary.
These two methods are the most straightforward way to add custom validation to a form, but not the only way. If you find yourself writing the same custom validation checks for multiple forms, writing your own custom validators will reduce repeating code. Check out the form and field validation section of the Django documentation to learn more.