Django のフォームバリデーションは、is_valid() を呼び出したときに自動的に実行されます。フィールド型に組み込まれた基本検証だけでなく、バリデータの追加やクリーンメソッドの定義によって、独自の検証ロジックを柔軟に組み込めます。
バリデーションの実行順序
is_valid() を呼ぶと、内部では以下の順序でバリデーションが進みます。
エラーが発生した段階で、そのフィールドのバリデーションは中断されます。ただし他のフィールドの検証は続行されるため、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 | 正規表現パターン |
| URLValidator | URL 形式 |
独自のバリデータを関数として定義することも可能です。
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_title は title フィールドの基本バリデーション(型チェック・max_length など)が完了した後に呼び出されます。self.cleaned_data['title'] には型変換済みの安全な値が入っており、この値に対してカスタム検証を行います。
検証を通過した場合は値を return で返す必要があります。返し忘れると cleaned_data から値が消えてしまうため注意してください。
再利用可能な検証関数。複数のフォームで同じロジックを共有できる
フォーム固有のカスタム検証。データベースへの問い合わせを伴う検証に向く
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」の順で段階的に検証が行われます。単純な入力チェックはバリデータや組み込みフィールドに任せ、ビジネスロジックを伴う検証はクリーンメソッドに書く——この役割分担がバリデーション設計の基本方針になります。