[Django]-Correct Way to Validate Django Model Objects?

26👍

I had a similar misunderstanding of the ORM when I first started with Django.

  1. No, don’t put self.full_clean() inside of save. Either

A) use a ModelForm (which will cause all the same validation to occur – note: ModelForm.is_valid() won’t call Model.full_clean explicitly, but will perform the exact same checks as Model.full_clean). Example:

class PersonForm(forms.ModelForm):
    class Meta:
        model = Person

def add_person(request):
    if request.method == 'POST':
        form = PersonForm(request.POST, request.FILES)
        if form.is_valid():  # Performs your validation, including ``validate_gender``
            person = form.save()
            return redirect('some-other-view')
    else:
        form = PersonForm()
        # ... return response with ``form`` in the context for rendering in a template

Also note, forms aren’t for use only in views that render them in templates – they’re great for any sort of use, including an API, etc. After running form.is_valid() and getting errors, you’ll have form.errors which is a dictionary containing all the errors in the form, including a key called '__all__' which will contain non-field errors.

B) Simply use model_instance.full_clean() in your view (or other logical application layer), instead of using a form, but forms are a nice abstraction for this.

  1. I don’t really have a solution to, but I’ve never run into such a problem, even in large projects (the current project I work with my company on has 146 tables) and I don’t suspect it’ll be a concern in your case either.

0👍

As of django 4.1, self.full_clean by default also runs self.validate_constraints (docs).

After makemigrations, there will be a migrations.AddConstraint statement included in the migration.

In your case:

from django.db import models

class Person(models.Model):
    class Gender(models.TextChoices):
        M = "M", "Male"
        F = "F", "Female"

    name = models.CharField(max_length=128)
    age = models.IntegerField()
    gender = models.CharField(maxlength=1, choices=Gender.choices)

    class Meta:
        constraints = [
            models.CheckConstraint(
                check=models.Q(gender__in=Gender.values), name="valid_gender"
            ),
            models.CheckConstraint(
                check=models.Q(age__gte=18), name="adults_only"
            ),
        ]

After running migrations, if you now try to self.save(), will raise an IntegrityError!

Leave a comment