フォームとフィールドの検証

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

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

Most validation can be done using validators - helpers that can be reused. Validators are functions (or callables) that take a single argument and raise ValidationError on invalid input. Validators are run after the field's to_python and validate methods have been called.

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

  • 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() メソッドがすでに一度データをクリーニングしているからです)。

    For example, if you wanted to validate that the contents of a CharField called serialnumber was unique, clean_serialnumber() would be the right place to do this. You don't need a specific field (it's a CharField), but you want a formfield-specific piece of validation and, possibly, cleaning/normalizing the data.

    このメソッドの戻り値は 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's form (and model) fields support use of utility functions and classes known as validators. A validator is a callable object or function that takes a value and returns nothing if the value is valid or raises a ValidationError if not. These can be passed to a field's constructor, via the field's validators argument, or defined on the Field class itself with the default_validators attribute.

Validators can be used to validate values inside the field, let's have a look at Django's SlugField:

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

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

As you can see, SlugField is a CharField with a customized validator that validates that submitted text obeys to some character rules. This can also be done on field definition so:

slug = forms.SlugField()

これは以下と同じです:

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

一般的なケース (例えば、E メールや正規表現に対する検証) は、Django が提供する既存のバリデータクラスを使って処理できます。例えば、validators.validate_slugRegexValidator の第 1 引数をパターン ^[-a-zA-Z0-9_]+$ としたインスタンスです。バリデータを記述する のセクションを参照して、利用可能なバリデータのリストとバリデータの記述方法の例を確認できます。

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

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

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)

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

Let's create a ContactForm to demonstrate how you'd use this field:

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

Use MultiEmailField like any other form field. When the is_valid() method is called on the form, the MultiEmailField.clean() method will be run as part of the cleaning process and it will, in turn, call the custom to_python() and validate() methods.

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

上の例を引き続き使用して、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."
                )

In this code, if the validation error is raised, the form will display an error message at the top of the form (normally) describing the problem. Such errors are non-field errors, which are displayed in the template with {{ 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)

The second argument of add_error() can be a string, or preferably an instance of ValidationError. See ValidationError を発生させる for more details. Note that add_error() automatically removes the field from cleaned_data.