コンテンツにスキップ

単一フィールドの検証#

2024 iThome鉄人レース

これはDjango Ninjaシリーズチュートリアルの第19回です。第5章:データ検証とエラー処理に入ります。

データ検証はAPI開発における重要な要件の一つであり、クライアントから送信されたデータが期待通りであることを確認し、潜在的なエラーやセキュリティ問題を回避する役割を担います。

効果的なデータ検証により、APIが誤ったデータを受け取った際に、即座にユーザーフレンドリーなレスポンスを返し、システムの安定性とユーザー体験を向上させることができます。

Django Ninjaにおけるデータ検証のコアツールはPydanticです。強力な検証機能を提供し、データ型のチェックを行うだけでなく、カスタム検証を簡単に実現できます。

この記事では、Django NinjaでPydanticを使用して単一フィールドのカスタム検証を実装する方法を紹介します。次回の記事では複数フィールド間のカスタム検証について説明します。

この記事のすべてのコード変更は、このPRを参照してください。

GitHubサンプルプロジェクト#

👉 Django-Ninja-Tutorial


第5章の概要#

データ検証は重要ですが、検証に失敗した場合、プログラムはしばしば検証エラーをスローします。これらのエラーを効果的に処理する方法は、「エラー処理」が議論すべき範疇です。

本章では、これら2つの密接に関連するテーマを探求し、合計4つの記事で構成されます:

前半の2つの記事では、入力データが期待通りであることを確保し、必要に応じてエラーをスローするための柔軟なデータ検証の実現方法を学びます。

後半の2つの記事では、より良いユーザー体験を提供するために、APIのプロセスで発生する可能性のある様々なエラー(検証エラーに限りません)の処理方法について議論します。

Django Ninjaのデータ検証とエラー処理のメカニズムは、Django REST frameworkに比べてより複雑であるため、それらを明確に理解してもらうために十分な紙幅を割いて紹介する必要があります。


APIの修正#

前回の記事で新しく作成したAPI——ユーザー追加——を例にします。

引き続きそれを改善し、カスタム検証を追加して、クライアントから送信されるデータをより信頼できるものにします。

ただし、先にいくつかのエラー修正を行います。修正後のコードは以下の通りです:

@router.post('/users/', summary='ユーザー追加', response={201: dict})
def create_user(...) -> tuple[int, dict]:
    """
    ユーザー追加
    """
    user = User(
        username=payload.username,
        email=payload.email,
        bio=payload.bio,
    )
    # set_passwordメソッドを使用してパスワードを暗号化
    user.set_password(raw_password=payload.password)
    user.save()
    return 201, {'id': user.id, 'username': user.username}

主に以下の2箇所を修正しました:

  1. routerデコレータにresponse={201: dict}パラメータを追加しました。元々は定義されていなかったため、実際にこのAPIを使用するとエラーが発生します。なぜならデフォルトでは200レスポンスのみであるため、200以外のレスポンスを求める場合は、responseパラメータを通じて宣言する必要があるからです。
  2. set_passwordメソッドを使用して、ユーザーが入力したパスワードを暗号化します。これはDjangoの組み込み機能であり、パスワードが直接DBに保存されるのを防ぎます。パスワードをプレーンテキストで保存してはいけないというのは、現代の開発における常識です。

修正が終わったので、本題に入りましょう。

異なる「レベル」の検証#

検証と言うからには、主な対象はもちろんクライアントからのリクエストに関連しています——リクエスト内容の検証です。

Django Ninjaでは、各APIはスキーマを定義することで、APIが受け取るデータ構造を記述できます。これらのスキーマはPydanticに基づいており、リクエスト内のデータを自動的に検証できます。

スキーマ内の型ヒント(型ヒント)はデータを検証でき、これが最も基本的な検証です。

前回の記事で言及したPydantic Fieldは、データの長さや範囲などの特性を検証できます。この部分については後ほどデモを行います。

これらはやや「形式的」な検証ですが、この記事では一定のルールに基づいたより複雑な「カスタム検証」に焦点を当てます。

サンプルAPIのスキーマの現状#

「ユーザー追加」を例にすると、リクエストボディ(request body)はusernameemailpasswordbioなどのフィールドを受け取ります。私たちが定義したスキーマを通じて、最も基本的なデータ型の検証を完了できます。

class CreateUserRequest(Schema):
    username: str
    email: str
    password: str
    bio: str | None = None

前回の記事で述べたように、bioフィールドのみがオプションであり、残りは必須です——欠けている場合は422レスポンスが返されます。したがって、スキーマは同時にデータの「存在性」も検証しています。

現在のところ悪くなさそうです!しかし、私たちはこれで満足しているわけではありません。


新しい要件:パスワードルール#

パスワードを設定する際、ユーザーに以下の2つのルールを守るよう要求します:

  1. パスワードの長さは少なくとも8文字。
  2. 少なくとも1つの数字を含める必要がある。

これらのルールはアカウントの安全性を高め、ユーザーが過度に簡単なパスワードを設定するのを防ぐのに役立ちます。

教育的な目的を考慮し、ルールをあまり複雑にしないようにしました。これら2つのルールにはそれぞれ特定の教育的意義があります:

  1. 最小文字数の制限は、自分で実装しなくてもPydantic Fieldを通じて直接実現できます。
  2. 2つ目のルールが目玉です。Pydanticの@field_validatorデコレータを使用して、フィールドの検証ルールを独自に定義します。

パスワードルールの検証の実装:field_validatorの使用#

要件に基づいて、まずPydanticのFieldを利用して最小文字数の制限を設定できます:

password: str = Field(min_length=8, examples=['password123'])

上記のように、min_length=8パラメータを1つ追加するだけで済みます。

「数字を含める必要がある」という検証については、@field_validatorデコレータを使用して実装する必要があります。

field_validatorデコレータ#

Pydantic v1では、このデコレータの名前はvalidatorでしたが、v2でfield_validatorに変更されました。

Pydanticはv1からv2にかけて、多くの破壊的変更(breaking change)がありました。例えば、以前言及したexampleパラメータがexamplesになったのもその一例です。この点は注意に値します。

以下は修正後のスキーマです。field_validator部分のみに注目します:

class CreateUserRequest(Schema):
    ...
    password: str
    ...

    @field_validator('password')
    @classmethod
    def validate_password_contains_number(cls, v: str) -> str:
        """
        パスワードに少なくとも1つの数字が含まれているかを検証する
        """
        if not re.search(r'\d', v):
            raise ValueError('パスワードには少なくとも1つの数字を含める必要があります')
        return v

重要なポイントの解説#

  1. field_validatorデコレータにはパラメータを使用する必要があり、有効な値はフィールド名(例:password)です。
  2. サンプルでは示していませんが、複数のフィールドに適用することもできます。
    1. 書き方は@field_validator('フィールド1', 'フィールド2', ...)であり、さらには@field_validator('*')と書いてすべてのフィールドに適用することも可能です。
    2. ただし、これらのフィールドは同じ検証ロジックを実行するため、理論的には論理的に類似したフィールドであることに注意してください。
  3. 検証メソッドの名前は自分で定義できます。自分が分かりやすければどのように命名しても構いません。
    1. なぜなら、Pydanticは主にデコレータ上のフィールド名を見ているからです。
    2. これは、Django REST frameworkの検証メソッドがvalidate_<フィールド名>という命名パターンを採用しているのとは大きく異なります
  4. Pydanticの検証メソッドのパラメータ名の命名慣例はvですが、Django REST frameworkではvalueです。
  5. 慣例その2:検証メソッドは成功時には入力値をそのままreturnし、失敗時にはエラーをスローします。
  6. Pydanticの検証メソッドは「クラスメソッド」であるため、最初のパラメータはclsです。特別なのは、@classmethodデコレータを省略できる点です。なぜならPydanticが内部で処理しているからです。
    1. しかし、公式ドキュメントは依然として@classmethodを使用することを推奨しており、私たちもそれに従います。
    2. もし@classmethodデコレータを宣言する場合、その位置は検証メソッドに最も近い必要があります。

驚いたでしょうか?たった数行のコードに、これほど多くの見どころがあるのです!


実際のテスト#

パスワードの長さが不足している場合をテストすると、結果は以下のようになります:

{
    "detail": [
        {
            "type": "string_too_short",
            "loc": [
                "body",
                "payload",
                "password"
            ],
            "msg": "String should have at least 8 characters",
            "ctx": {
                "min_length": 8
            }
        },
        {
            "type": "string_too_short",
            "loc": [
                "body",
                "payload",
                "confirm_password"
            ],
            "msg": "String should have at least 8 characters",
            "ctx": {
                "min_length": 8
            }
        }
    ]
}
これはFieldのチェック時に独自にスローされるエラーであり、レスポンスステータスコードは422です。

次に、パスワードに数字が含まれていない場合をテストします:

{
    "detail": [
        {
            "type": "value_error",
            "loc": [
                "body",
                "payload",
                "password"
            ],
            "msg": "Value error, パスワードには少なくとも1つの数字を含める必要があります",
            "ctx": {
                "error": "パスワードには少なくとも1つの数字を含める必要があります"
            }
        }
    ]
}
これは私たちが定義した「半カスタム」のエラーレスポンスと言えます。構造は依然としてDjango Ninjaによって決定されますが、エラーメッセージ部分は私たちが独自に定義したものだからです。

エラーレスポンスのカスタマイズはさらに柔軟に行うことができますが、これは次々回の記事「エラー処理(上)HttpErrorとカスタムHTTPレスポンス」のテーマですので、その時に詳しく議論しましょう。


まとめ#

この記事では、Pydanticを通じて単一フィールドに対するデータ検証を行い、パスワードの強度チェックルールを実装する方法を学びました。

次回は、このテーマをさらに掘り下げ、より複雑な複数フィールド間の検証を実現します。