フォームとフィールドのバリデーション

フォームのバリデーションは、データがクリーニング (clean) されるときに実行されます。このプロセスをカスタムしたい場合は、様々な箇所に変更を加えることができ、それぞれが違う目的を持っています。クリーニング方法のうち 3 タイプは、フォームのプロセス中に実行されます。これらは、通常フォーム上の is_valid() メソッドを呼び出したときに実行されます。このほかにも、クリーニングとバリデーションのトリガーとなる処理 (直接 errors 属性にアクセスしたり、 full_clean() を直接呼び出す) がありますが、通常必要とはなりません。

一般的に、あらゆるクリーニング方法は ValidationError を投げる可能性があります。処理されるデータに問題がある場合、関連情報を ValidationError コンストラクタに渡します。下記項目 に、ValidationError を投げる際のベストプラクティスがあります。ValidationError が投げられない場合、メソッドはクリーニングされた (標準化された) データを Python オブジェクトとして返すはずです。

ほとんどのバリデーションは validators を使用して行うことができます。これは再利用できるヘルパーです。バリデーション用の関数(または呼び出し可能オブジェクト)は、単一の引数を取り、無効な入力の場合に ValidationError を発生させます。バリデーション用の関数は、フィールドの to_python メソッドと validate メソッドが呼び出された後に実行されます。

フォームのバリデーションは複数のステップに分割されます。カスタムやオーバーライドもできます:

  • Fieldto_python() メソッドはすべてのバリデーションの最初のステップとなります。これは、値をデータ型に正すことを強制し、それができない場合は ValidationError を投げます。このメソッドはウィジェットからそのままの値を受け取り、変換した値を返します。たとえば、FloatField は Python の float に変換されるか、もしくは ValidationError を投げます。

  • Fieldvalidate() メソッドは、フィールド特有のバリデーションを行います。これはバリデータとしては不適です。正しいデータ型を強制された値を取り、エラーの際は ValidationError を投げます。このメソッドは何も返さず、また値の変更も行わないはずです。バリデータに記述したくないバリデーションロジックを実行するためには、このメソッドをオーバーライドしてください。

  • Fieldrun_validators() メソッドはフィールドのバリデータをすべて実行し、すべてのエラーを単一の ValidationError に統合します。このメソッドをオーバーライドする必要はないはずです。

  • Field サブクラスの clean() メソッドは、to_python()validate()run_validators() を正しい順序で実行し、エラーを伝達する役目を負っています。もしこの過程のどこかで ValidationError が投げられた場合、バリデーションは停止し、そのエラーを投げます。このメソッドはクリーニングされたデータを返し、フォームの cleaned_data ディクショナリに格納します。

  • clean_<fieldname>() メソッドは、フォームサブクラス上で呼び出されます -- <fieldname> がフォームのフィールド属性の名前と置き換えられます。このメソッドは、フィールドのタイプにかかわらず、その特定の属性に対するクリーニングを実行します。 このメソッドはパラメータを受け取りません。self.cleaned_data 内のフィールドの値を検索する必要があり、またこの時点ではフォーム上で元々送信された文字列ではなく Python オブジェクトであること覚えておいてください (これは cleaned_data 内にあります。上記に出てきた一般フィールドの clean() メソッドがすでに一度データをクリーニングしているからです)。

    例えば、 serialnumber という CharField の内容が一意であることを検証したい場合、 clean_serialnumber() が適切な場所になります。特定のフィールドは必要ありません (CharField です) が、フォームフィールド固有のバリデーションや、データのクリーニング/正規化が必要です。

    このメソッドの戻り値は cleaned_data 内の既存の値を置き換えるため、(たとえこのメソッドが変更しなかったとしても) cleaned_data からのフィールドの値、ないし新しくクリーニングされた値になるはずです。

  • フォームのサブクラスの clean() メソッドは、複数のフォームフィールドにまたがるバリデーションにも使用できます。この場所には、たとえば「フィールド A が入力されたときフィールド B は有効なメールアドレスのはず」といったチェックを記述できます。このメソッドは、必要な場合はまったく違うディクショナリを返すこともでき、その場合は cleaned_data として使用されます。

    フィールドバリデーションのメソッドは clean() が呼ばれる際に実行されるので、フォームの errors 属性にもアクセスできるようになります。これは各フィールドのクリーニングによって発生した例外をすべて含みます。

    Form.clean() をオーバーライドして発生させた例外は、特定のフィールドに結びつかない点に注意してください。これらは特別な "フィールド" (__all__ と呼ばれます) に格納され、必要に応じて non_field_errors() メソッドを通じてアクセスできます。特定のフィールドにエラーを紐付けて格納したい場合は、add_error() を呼び出す必要があります。

    ModelForm サブクラスの clean() メソッドをオーバーライドする際、特に考慮する点がまだあります (詳しくは ModelForm ドキュメント を参照してください)。

これらのメソッドは、一度に一つのフィールドに対し、先述した通りの順番で実行されます。フォーム内の各フィールドに対して (フォームの定義時に宣言された順序で)、Field.clean() メソッド (ないしそのオーバーライド) が実行され、その後 clean_<fieldname>() が実行されます。最後に、この 2 つのメソッドが全フィールドに対して実行された後、これらのメソッドでエラーが発生したかどうかにかかわらず Form.clean() メソッド (ないしそのオーバーライド) が実行されます。

各メソッドの例は後述します。

上述の通り、どのメソッドでも ValidationError が発生する可能性があります。どのフィールドに対しても、Field.clean() メソッドが ValidationError を発生させた場合、フィールド特有のクリーニングメソッドは呼ばれません。一方で、すべての残りのフィールドに対するクリーニングメソッドは呼び出されます。

ValidationError を発生させる

エラーメッセージを柔軟かつ簡単にオーバーライドできるようにするため、以下のガイドラインを検討してください:

  • 説明のための code をコンストラクタに渡します:

    # Good
    ValidationError(_("Invalid value"), code="invalid")
    
    # Bad
    ValidationError(_("Invalid value"))
    
  • メッセージには変数を強制しません; プレースホルダとコンストラクタの params 引数を使用します:

    # Good
    ValidationError(
        _("Invalid value: %(value)s"),
        params={"value": "42"},
    )
    
    # Bad
    ValidationError(_("Invalid value: %s") % value)
    
  • 位置指定ではなく、マッピングキーを使います。これにより、メッセージを書き直すときに、変数を任意の順序で配置したり、すべて省略したりすることができます。

    # Good
    ValidationError(
        _("Invalid value: %(value)s"),
        params={"value": "42"},
    )
    
    # Bad
    ValidationError(
        _("Invalid value: %s"),
        params=("42",),
    )
    
  • メッセージを gettext でラップし、翻訳できるようにします:

    # Good
    ValidationError(_("Invalid value"))
    
    # Bad
    ValidationError("Invalid value")
    

全てを一緒に記述すると以下のようになります:

raise ValidationError(
    _("Invalid value: %(value)s"),
    code="invalid",
    params={"value": "42"},
)

再利用可能なフォーム、フォームフィールド、モデルフィールドを記述した場合、特にこのガイドラインの遵守が必要となります。

推奨はされませんが、バリデーションチェーンの最後で (たとえばフォームの clean() メソッド) エラーメッセージのオーバーライドを 決してしない ことが確かな場合は、より簡潔に記述することもできます:

ValidationError(_("Invalid value: %s") % value)

Form.errors.as_data()Form.errors.as_json() メソッドは、十分な機能を有する (code 名と params ディクショナリを持つ) ValidationError により大きな恩恵を受けます。

複数のエラーを起こす

クリーニングメソッド内で複数のエラーを検証し、すべてのエラーをフォーム送信者に知らせたい場合、ValidationError コンストラクタにエラーのリストを渡すことができます。

上述の通り、ValidationError インスタンスには codeparams を渡すことが推奨されていますが、文字列のリストも使うことができます:

# Good
raise ValidationError(
    [
        ValidationError(_("Error 1"), code="error1"),
        ValidationError(_("Error 2"), code="error2"),
    ]
)

# Bad
raise ValidationError(
    [
        _("Error 1"),
        _("Error 2"),
    ]
)

実際にバリデーションを使用する

前のセクションでは、フォームに対する検証が一般にどのように働くかを説明しました。実際の使われ方を見た方が機能をよく理解できるということが往々にしてあります。ここでは、説明した各機能を使った一連の小さな使用例を説明します。

バリデータを使う

Django のフォーム(とモデル)フィールドは、バリデータとして知られるユーティリティ関数やクラスの使用をサポートしています。バリデータは呼び出し可能なオブジェクトや関数で、値を受け取り、その値が有効であれば何も返さず、有効でなければ ValidationError を発生させます。バリデータはフィールドのコンストラクタに渡すか、フィールドの validators 引数で渡すか、 Field クラスの default_validators 属性で定義します。

バリデータはフィールド内の値を検証するために使用できます。Django の SlugField を見てみましょう:

from django.core import validators
from django.forms import CharField


class SlugField(CharField):
    default_validators = [validators.validate_slug]

SlugField は、特定の文字規則に従うテキストを受け付けるように検証するカスタムバリデータを持つ CharField です。これは、フィールド定義時にも行うことができます。

slug = forms.SlugField()

これは以下と同じです:

slug = forms.CharField(validators=[validators.validate_slug])

メールアドレスや正規表現に対する検証などの一般的なケースは、Django に既存のバリデーションクラスを使用して処理できます。例えば、validators.validate_slug は、最初の引数としてパターン ^[-a-zA-Z0-9_]+\Z を指定して構築された RegexValidator のインスタンスです。利用可能なバリデータの一覧や、バリデータの書き方の例については、バリデータの作成 のセクションを参照してください。

フォームフィールドのデフォルトのクリーニング

まず、カンマで区切られたメールアドレスを含むかどうか検証するカスタムフォームフィールドを作成しましょう。クラスの全体像は以下のようになります:

from django import forms
from django.core.validators import validate_email


class MultiEmailField(forms.Field):
    def to_python(self, value):
        """Normalize data to a list of strings."""
        # Return an empty list if no input was given.
        if not value:
            return []
        return value.split(",")

    def validate(self, value):
        """Check if value consists only of valid emails."""
        # Use the parent's handling of required fields, etc.
        super().validate(value)
        for email in value:
            validate_email(email)

このフィールドを使うすべてのフォームでは、フィールドのデータを使えるようになる前に、これらのメソッドが実行されます。これはこのタイプのフィールドに特有であり、それはこの後の使われ方には関係ありません。

このフィールドの使い方を示すために、ContactForm を作成してみましょう:

class ContactForm(forms.Form):
    subject = forms.CharField(max_length=100)
    message = forms.CharField()
    sender = forms.EmailField()
    recipients = MultiEmailField()
    cc_myself = forms.BooleanField(required=False)

MultiEmailField は他のフォームフィールドと同様に使用します。フォームに対して is_valid() メソッドが呼び出されると、クリーニングプロセスの一環として MultiEmailField.clean() メソッドが実行され、それがさらにカスタムの to_python() および validate() メソッドを呼び出します。

特定のフィールド属性をクリーニングする

上の例を引き続き使用して、ContactFormrecipients フィールドが常に "fred@example.com" を含むようにしたいとします。これはフォームに特有のバリデーションなので、MultiEmailField クラスには記述したくありません。代わりに recipients フィールドで動作するクリーニングメソッドを記述します:

from django import forms
from django.core.exceptions import ValidationError


class ContactForm(forms.Form):
    # Everything as before.
    ...

    def clean_recipients(self):
        data = self.cleaned_data["recipients"]
        if "fred@example.com" not in data:
            raise ValidationError("You have forgotten about Fred!")

        # Always return a value to use as the new cleaned data, even if
        # this method didn't change it.
        return data

互いに依存するフィールドをクリーニングして検証する

コンタクトフォームに新たな要件を追加することを考えます: cc_myself フィールドが True の場合、subject には必ず "help" という言葉が含まれるという要件です。この場合、複数のフィールドにまたがったバリデーションを行うので、フォームの clean() メソッドで行うのが適切です。ここで注意すべきなのは、今扱っているのはフォームの clean() メソッドであり、上記で扱ってきたフィールドの clean() ではないということです。バリデーションを記述すする場所を決める際は、フィールドとフォームの違いを明確にすることが重要です。 フィールドは単一のデータポイントであり、フォームはフィールドの集まりです。

フォームの clean() メソッドが呼び出されるまでに、個別のフィールドのクリーンメソッドが呼び出されているので (上記 2 つのセクションで見た通りです)、self.cleaned_data にはこれまでのクリーニングで生き残ったデータが格納されています。したがって、検証しようとしているフィールドが、この個別フィールドのチェックを生き残っていない可能性を考慮する必要があります。

この段階でのエラーを通知するには 2 つの方法があります。おそらく最も一般的な方法は、フォームのトップでエラーを表示する方法です。このようなエラーを生成するには、clean() メソッドで ValidationError を発生させてください。次に例を示します。

from django import forms
from django.core.exceptions import ValidationError


class ContactForm(forms.Form):
    # Everything as before.
    ...

    def clean(self):
        cleaned_data = super().clean()
        cc_myself = cleaned_data.get("cc_myself")
        subject = cleaned_data.get("subject")

        if cc_myself and subject:
            # Only do something if both fields are valid so far.
            if "help" not in subject:
                raise ValidationError(
                    "Did not send for 'help' in the subject despite CC'ing yourself."
                )

このコードでは、検証エラーが発生した場合、通常、問題を説明するエラーメッセージがフォームの上部に表示されます。このようなエラーは、フィールド以外のエラーとされ、テンプレート内で {{ form.non_field_errors }} として表示されます。

例のコードにある super().clean() の呼び出しは、親クラスのバリデーションロジックも維持されることを保証します。clean() メソッドで (必須ではないので) cleaned_data ディクショナリを返さないクラスを継承している場合、cleaned_datasuper() の結果にアサインせず、代わりに self.cleaned_data を使用してください:

def clean(self):
    super().clean()
    cc_myself = self.cleaned_data.get("cc_myself")
    ...

バリデーションエラーを通知するもう 1 つのアプローチは、エラーメッセージをフィールドの 1 つにアサインすることです。この場合、エラーメッセージはフォーム表示の "subject" と "cc_myself" の両方の行にアサインしましょう。この方法をとるときは、フォームの出力に混乱を招かないように注意してください。ここでは何が可能なのかを示しますが、実際の状況で効果的に実装するのはあなたとあなたのデザイナー次第です。(上の例を置き換えた) 新しいコードは以下のようになります:

from django import forms


class ContactForm(forms.Form):
    # Everything as before.
    ...

    def clean(self):
        cleaned_data = super().clean()
        cc_myself = cleaned_data.get("cc_myself")
        subject = cleaned_data.get("subject")

        if cc_myself and subject and "help" not in subject:
            msg = "Must put 'help' in subject when cc'ing yourself."
            self.add_error("cc_myself", msg)
            self.add_error("subject", msg)

add_error() の第2引数は、文字列でもよいですが、できれば ValidationError のインスタンスが最適です。詳細は、ValidationError を発生させる を参照してください。なお、add_error() は自動的にフィールドを cleaned_data から削除します。

Back to Top