Skip to content

Model Validation

Visitors are going to submit adoption applications from the site, and the submitted data has to be validated before it's saved. Our fields already have rules: species has a set of choices, name has a maximum length, and an application's email has to be an email address.

This chapter's starting branch provides a phone number validator and the logging settings. It also provides the application form's page: its template, view, and URL pattern. Check it out before we start:

shell
git stash -u
git fetch --all
git checkout -b my-ch06-start --no-track origin/ch06-start
uv sync

If Git prints No local changes to save, there was nothing to set aside.

Saving Doesn't Validate

save() writes a row without checking those rules, so an animal with a species of "dragon" would be saved. Model validation checks them, and full_clean() runs it:

  • each field against its rules, such as its choices and its maximum length
  • then the model's clean()

It raises a ValidationError that lists what failed. The admin's forms call full_clean(), which is how the admin refuses a second approval. The form we add will too.

Why doesn't save() validate?

Validation and saving are separate jobs: validation decides whether data is acceptable and says what's wrong, and save() writes it.

Field Validators

Right now an application's phone number can be any text up to 20 characters, even though its help text asks for a US number. We need a rule that checks the number before an application is saved.

A custom rule goes on a field as a validator: a callable that takes a value and raises ValidationError when the value isn't acceptable. Validators for common rules are in django.core.validators, and RegexValidator checks a value against a regular expression.

The starting branch provides validate_phone in animals/validators.py. It's a RegexValidator for a US phone number: ten digits, with or without parentheses around the area code, and with or without a space or a hyphen between the groups. 555-010-2000, (555) 010-2000, and 5550102000 pass, and 555-1234 doesn't.

A model field takes a list of validators in its validators argument. In animals/models.py, import validate_phone:

animals/models.py
from datetime import date, timedelta

from django.core.exceptions import ValidationError
from django.db import models

from .validators import validate_phone

Then add it to phone:

animals/models.py
    phone = models.CharField(
        max_length=20,
        blank=True,
        help_text="A US number, like 555-010-2000",
        validators=[validate_phone],
    )

Model validation runs each field's own checks, then its validators. A validator doesn't run on an empty value, so phone can still be left blank.

The change needs a migration:

shell
uv run manage.py makemigrations
uv run manage.py migrate