Django Custon 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.

Multi-Field Validation

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[0].lower() != last_name[0].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.

Summary

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 documentationto learn more.