Django のバリデーションの仕組み(clean メソッドとバリデータ)

Django のフォームバリデーションは、is_valid() を呼び出したときに自動的に実行されます。フィールド型に組み込まれた基本検証だけでなく、バリデータの追加やクリーンメソッドの定義によって、独自の検証ロジックを柔軟に組み込めます。

バリデーションの実行順序

is_valid() を呼ぶと、内部では以下の順序でバリデーションが進みます。

各フィールドの to_python()(型変換)
各フィールドの validate()(フィールド固有の検証)
各フィールドの run_validators()(バリデータの実行)
各フィールドの clean_フィールド名()(フィールド単位のカスタム検証)
フォーム全体の clean()(複数フィールドにまたがる検証)

エラーが発生した段階で、そのフィールドのバリデーションは中断されます。ただし他のフィールドの検証は続行されるため、1 回の送信ですべてのエラーをまとめて表示できます。

バリデータ(validators)

バリデータは単一の値を受け取り、条件を満たさない場合に ValidationError を送出する関数またはクラスです。フィールド定義時に validators パラメータで指定します。

from django.core.validators import MinLengthValidator, RegexValidator

class SignupForm(forms.Form):
    username = forms.CharField(
        max_length=30,
        validators=[
            MinLengthValidator(3, message='3文字以上で入力してください'),
            RegexValidator(
                regex=r'^[a-zA-Z0-9_]+$',
                message='英数字とアンダースコアのみ使用できます'
            ),
        ]
    )

Django には便利な組み込みバリデータが用意されています。

バリデータ検証内容
MinLengthValidator最小文字数
MaxLengthValidator最大文字数
MinValueValidator最小値(数値)
バリデータ検証内容
MaxValueValidator最大値(数値)
RegexValidator正規表現パターン
URLValidatorURL 形式

独自のバリデータを関数として定義することも可能です。

from django.core.exceptions import ValidationError

def validate_no_profanity(value):
    forbidden = ['spam', 'abuse']
    for word in forbidden:
        if word in value.lower():
            raise ValidationError(
                f'{word}」は使用できません。'
            )

class CommentForm(forms.Form):
    text = forms.CharField(
        widget=forms.Textarea,
        validators=[validate_no_profanity]
    )

バリデータは再利用しやすいため、複数のフォームで同じ検証ロジックを共有したいときに適しています。

clean_フィールド名(フィールド単位の検証)

特定のフィールドに対するカスタム検証は、clean_フィールド名 というメソッドで定義します。

class ArticleForm(forms.ModelForm):
    class Meta:
        model = Article
        fields = ['title', 'body']

    def clean_title(self):
        title = self.cleaned_data['title']
        if Article.objects.filter(title=title).exists():
            raise ValidationError('同じタイトルの記事がすでに存在します。')
        return title

clean_titletitle フィールドの基本バリデーション(型チェック・max_length など)が完了した後に呼び出されます。self.cleaned_data['title'] には型変換済みの安全な値が入っており、この値に対してカスタム検証を行います。

検証を通過した場合は値を return で返す必要があります。返し忘れると cleaned_data から値が消えてしまうため注意してください。

validators

再利用可能な検証関数。複数のフォームで同じロジックを共有できる

clean_フィールド名

フォーム固有のカスタム検証。データベースへの問い合わせを伴う検証に向く

clean(フォーム全体の検証)

複数のフィールドにまたがるバリデーション——たとえば「パスワードと確認パスワードの一致」——は clean メソッドで行います。

class PasswordChangeForm(forms.Form):
    password = forms.CharField(widget=forms.PasswordInput, label='新しいパスワード')
    password_confirm = forms.CharField(widget=forms.PasswordInput, label='パスワード確認')

    def clean(self):
        cleaned_data = super().clean()
        password = cleaned_data.get('password')
        password_confirm = cleaned_data.get('password_confirm')

        if password and password_confirm and password != password_confirm:
            raise ValidationError('パスワードが一致しません。')

        return cleaned_data

clean メソッドの中では super().clean() を最初に呼び出し、各フィールドの検証結果を取得します。get で値を取り出しているのは、先行するフィールドバリデーションでエラーが発生した場合に cleaned_data にキーが存在しないことがあるためです。

clean メソッドで raise された ValidationError は、特定のフィールドではなくフォーム全体のエラー(non_field_errors)として扱われます。テンプレートでは次のように表示します。

{{ form.non_field_errors }}
<form method="post">
    {% csrf_token %}
    {{ form.as_p }}
    <button type="submit">変更</button>
</form>

特定フィールドにエラーを紐づける

clean メソッド内で、エラーを特定のフィールドに紐づけたい場合は add_error を使います。

def clean(self):
    cleaned_data = super().clean()
    start = cleaned_data.get('start_date')
    end = cleaned_data.get('end_date')

    if start and end and start > end:
        self.add_error('end_date', '終了日は開始日より後にしてください。')

    return cleaned_data

add_error を使うと、エラーメッセージがフォーム全体ではなく指定したフィールドに表示されるため、ユーザーにとってどこを直せばよいかが明確になります。

ModelForm でのバリデーション

ModelForm ではフォームのバリデーションに加えて、モデルの unique 制約や unique_together 制約も自動的に検証されます。

class Article(models.Model):
    title = models.CharField(max_length=200, unique=True)
    slug = models.SlugField(unique=True)

この Article モデルに対して ModelForm を使えば、タイトルやスラッグの重複チェックをフォーム側に手書きする必要はありません。

バリデーションの階層構造をまとめると、「フィールド型の基本検証 → バリデータ → clean_フィールド名 → clean」の順で段階的に検証が行われます。単純な入力チェックはバリデータや組み込みフィールドに任せ、ビジネスロジックを伴う検証はクリーンメソッドに書く——この役割分担がバリデーション設計の基本方針になります。