リクエストボディとスキーマ#

これはDjango Ninjaシリーズチュートリアルの第12回です。
これまでの紹介で、パスとクエリパラメータの処理方法を学びました。しかし現実世界では、しばしばより複雑なリクエストデータを処理する必要があります。
例えば、ユーザーが送信するフォームやアップロードするファイルなどです。APIにおいて最も一般的なのは、JSON形式のリクエストボディ(request body)です。
この記事では、Django Ninjaがどのようにリクエストボディを処理するかを探求し、スキーマを通じてデータを定義および検証する方法を紹介します。
この記事のすべてのコードの変更は、こちらのPRを参考にしてください。
GitHubサンプルプロジェクト#
1. リクエストボディとは?#
リクエストボディとは、HTTPリクエストと共に送信されるデータを指し、通常はPOSTやPUTなどの「リソース」の作成や更新を必要とするリクエストに使用されます。
これらのデータはURLには表示されず、JSONやその他の形式(XML、form-dataなど)としてリクエストの主体となります。
例えば、ユーザーが新しい記事を投稿する際、以下のようなJSON形式のリクエストボディを送信するかもしれません:
このリクエストボディにはtitleとcontentの2つのフィールドが含まれており、Django Ninjaはこれらのデータを処理し、検証するのを支援します。
2. サンプルプロジェクトの変更#
サンプルプロジェクトに、リクエストボディを受信するAPIである「記事の追加」を作成します。
さらに、Djangoのpost appディレクトリの下に、Pythonモジュールschemas.pyを追加します。ここは、APIで使用されるすべてのスキーマを配置するための場所です。
具体的なコードについては、今後の説明で紹介します。
この記事から、ブランチ名に中国語を使用しないことにしました。なぜなら、中国語のブランチ名はGitHubに常に警告されるからです:
The head ref may contain hidden characters: ...
また、gitブランチの命名に中国語を使用する人はほとんどいないでしょう!当初中国語を使用した理由は、読者が読みやすいようにするためでした🥹。
ですので、このブランチからは「数字+英語」に変更します。例えば、この記事は「12-request-body」とします。ただし、PRのタイトルは引き続き中国語のまま維持します。
3. スキーマを使用したリクエストボディの定義と検証#
FastAPIと同様に、Django NinjaはPydantic BaseModelを使用してリクエストボディを処理します。
ただし、BaseModelという名前はDjangoのModelsと混同しやすいため、Django Ninjaはこれをスキーマに名前変更しました。
スキーマはBaseModelを継承しているため、両者の実際の内容は非常に近いです(Django Ninjaは少し手を加えています):
プロジェクトに戻り、プロジェクト内の例を見てみましょう。これは「記事の追加」APIのリクエストボディのスキーマを定義するものです:
# post/schemas.py
from ninja import Schema
class CreatePostRequest(Schema):
title: str
content: str
user_id: int
このスキーマは、ボディデータにこれら3つのフィールド(title、content、user_id)が必ず含まれていること、そしてデータの型も一致していることを要求します。
view関数でのスキーマの使用#
「リクエスト」スキーマが定義できたら、view関数の中で「関数の引数」として使用できます:
from post.schemas import CreatePostRequest
...
@router.post(path='/posts/')
def create_post(..., payload: CreatePostRequest): # ここ
...
関数のpayloadパラメータの型ヒント(型ヒント)を、先ほど定義したCreatePostRequestに設定します。
このAPIにリクエストが送信されると、Django NinjaはCreatePostRequestというスキーマを通じて、ボディ内のデータを解析(parsing)し、検証します。
検証が成功した後、データをview関数のpayloadに渡します。この時、関数内部のpayloadパラメータは、本質的にスキーマ(すなわちPydantic BaseModel)オブジェクトとなります。
自動データ検証とエラーハンドリング#
リクエストボディにフィールドが欠けていたり、データの型が間違っていたりする場合、Django Ninjaは自動的に422レスポンスを返し、具体的なエラー情報を提供します:
{
"detail": [
{
"type": "missing",
"loc": [
"body",
"payload",
"content"
],
"msg": "Field required"
}
]
}
エラーメッセージは、ボディにcontentというフィールドが欠けていることを示しています。
4. 任意の(Optional)フィールドとデフォルト値#
実際のAPI開発において、すべてのリクエストフィールドが必須であるとは限りません。
Pydanticと型ヒントを通じて、任意のフィールドを定義することができます。仮に、現在の記事のコンテンツが完全に任意であるとしましょう:(contentフィールドに注目してください)
=演算子を使用して、contentフィールドのデフォルト値をNoneに設定すると、そのフィールドは任意のフィールドになります。この時、contentの型ヒントもstr | Noneに変更する必要があります。
特筆すべきは、スキーマをリクエストに使用する場合、このように設定して検証を通過できたとしても、対応するDjangoモデルフィールド(つまりdbフィールド)がNULLを許可しているかに注意する必要があることです。そうしないとエラーが発生します:
django.db.utils.IntegrityError: NOT NULL constraint failed: post_post.content
フィールドを任意にするだけでなく、直接デフォルト値を与えることもできます。例えばここでの空文字列です。ユーザーが入力しなかった場合、自動的にデフォルト値が入力されます:
しかし、デフォルト値がNoneである場合を除き、スキーマ内でデフォルト値を与える行為は「非常に慎重に」行う必要があります。この部分については、「第18回:Pydantic Fieldの設定例とデフォルト値」で再度議論します。
5. Django Ninjaの引数判定順序#
view関数の引数には多くの種類があるのに、Django Ninjaはどうやってどれがどれに対応するかを知るのか、考えたことはありますか?
実際には、Django Ninjaはview関数のシグネチャに基づいて、パラメータの出所(パスパラメータ、クエリパラメータ、あるいはリクエストボディのいずれか)を自動的に判断します。その判定順序は以下の通りです:
- パスパラメータ:URL path内に定義された変数(例えば
/items/{id}のid)は、最優先でパスパラメータとして認識されます。 - クエリパラメータ:関数内のその他の単数型パラメータ(例えば
listやdictではなく、int、float、bool、str)は、パスパラメータとしてマークされていない場合、クエリパラメータとして認識されます。 - リクエストボディ:スキーマ型のパラメータのみが、リクエストボディと見なされます。
原則として、view関数は1つのスキーマパラメータしか持つことができません。1つのリクエストにはボディが1つしかないからです。
第2節の終わり#
本節の内容はほぼ終了しました。
この節では、Django Ninjaを使用してHTTPリクエストを処理する方法を学び、スキーマの基本的な使い方を紹介しました。
スキーマの使い方や変化はまだたくさんあり、ここはほんの「手始め」に過ぎません。第3節「HTTPレスポンス」では、スキーマの設定についてさらに多くを目にすることでしょう。
次の節に入る前に、小休止といくつかの準備を行いましょう。
小休止と準備#
次の節では、プロジェクトのAPIを実際に稼働させます。なぜ現在使用できないのか、先述した内容を覚えていますか?
- dbデータがない。
- スキーマが作成されていない。
私たちはすでにスキーマの使い方を学びました——まだ完全ではありませんが。それでは、「dbデータ」の問題も解決する必要があります。
Django Fixtures#
POST APIを呼び出して、ユーザーや記事のデータを手動で追加することもできますが、面倒すぎます!ましてや、プロジェクトには現在「ユーザーの追加」というAPIはまだありません。
ですので、面倒なことはやめましょう。
直接Django fixturesを使用して、私が事前に定義したダミーデータをインポートします。
Django fixturesの紹介については、記事「Django Fixtureを使用してデータをインポート・エクスポートする」を参照してください。
次の記事の13-responseブランチの進捗の配下には、私がエクスポートしたfixturesデータがすでに確認できます:
users.json。posts.json。
これらを使用したい場合は、直接順番にインポートするだけです:
必ず先にusersをインポートしてください。そうしないと、記事に作者が存在せず、関連付けに失敗します。
インポートが完了すると、AliceとBobという2人のユーザーと、彼らがそれぞれ投稿した30件の記事を取得できます。

ええと、私の最初のテスト記事が混ざっていますが、ご容赦ください😅。
インポートに成功したら、次へ進みましょう。