MENU

Django Ninjaチュートリアル⑩(レスポンススキーマ)

Django-Ninjaチュートリアル⑩(レスポンススキーマ)

前回までの記事はこちらです。

前回までの記事が読まれている前提で書いていきますので、まだ読んでいない方はこちらから読み進めて追いついてください。

それでは始めていきましょう。

目次

レスポンススキーマ

Django Ninjaではバリデーションと説明付きの情報設定を目的として、リクエストに対するSchemaを設定することができます。

ユーザー作成のAPIを作るとして、作成時の入力パラメータは、usernamepasswordとすることが多いですが、これに対するユーザー情報の出力を考えた場合、出力はuser_idusername(ただし、通常セキュリティの問題上、パスワードは出力しません) であるべきだと考えられます。

これをSchemaで考えていきます。

Schemaの作成

それでは初めにSchemaを作成していきます。

from ninja import Schema

class UserIn(Schema):
  username: str
  password: str

User inクラスは、ユーザー作成時に要求する項目として、usernamepasswordを定義しています。

次に、これを使って、ユーザー作成のAPIを作成します。

from ninja import NinjaAPI

from example.Schema.api_v4_schema import UserIn
from .models import User

app_v4 = NinjaAPI(
  title = "Django-Ninja Sample App4",
  version = "1.0.4",
)

# add 1
@app_v4.post("/users/")
def create_user(request, data: UserIn):
  user = User(username = data.username)
  user.set_password(data.password)
  user.save()

また、プロジェクトルートのurls.pyで今回作成したAPIを認識させる必要がありますので、下記のように記述します。

from django.contrib import admin
from django.urls import path
from example.api_v1 import app
from example.api_v2 import app_v2
from example.api_v3 import app_v3
from example.api_v4 import app_v4

urlpatterns = [
  path('admin/', admin.site.urls),
  path("api/", app.urls),
  path("api_v2/", app_v2.urls),
  path("api_v3/", app_v3.urls),
  path("api_v4/", app_v4.urls), # add
]

次に、UserInにより作成されたUser情報を返すために、新たにUserOutというSchemaを定義します。

from ninja import Schema

class UserIn(Schema):
  username: str
  password: str

class UserOut(Schema):
  id: int
  username: str

さらに、作成したAPIに対し、UserOutを渡し、レスポンスとして返すSchemaをUserOutに指定します。

from ninja import NinjaAPI

from app.backend.example.Schema.api_v4_schema import UserIn
from register.models import User

app_v4 = NinjaAPI(
  title = "Django-Ninja Sample App4",
  version = "1.0.4",
)

# add 1
@app_v4.post("/users/")
def create_user(request, data: UserIn):
  user = User(username = data.username)
  user.set_password(data.password)
  user.save()

これにより、UserInで作成されたユーザーは、作成後に、UserOutで返され、新たにidが付与され、usernameと共に返されます。

モデルのインポート
モデルは先に作成したregisterアプリケーションの中で作成したモデルからUserモデルをインポートしています。
詳細は、以下の記事をご覧ください。

以下のようにAPIとSchemaが表示されれば成功です。

UserInとUserOut設定後のAPI

Try it outからユーザー作成をしてみると、下記のように作成されると思います。
このように、入力と出力を分けてSchemaを設定することで、レスポンスに対して別個のSchemaを設定することで、バリデーションを適用することができます。
また、入力の際に要求した項目のうち、出力してはいけない項目(今回はpassword)を、レスポンスモデルに出力用のSchemaを定義し、設定することで、出力する項目を制限することができます。

ユーザー作成の結果

Schemaのネスト

次に見ていくのは、Schemaのネストについてです。

まずは、今回の例に必要なモデルを定義します。
モデルはForighnKeyを持つTaskモデルとします。

from django.db import models

class Task(models.Model):
  title = models.CharField(max_length = 200)
  is_completed = models.BooleanField(default = False)
  owner = models.ForeignKey("register.User", null = True, blank = True on_delete = models.CASCADE)
  
    def __str__(self):
      return self.title  

それでは、新しくモデルを作成したため、マイグレーションをする必要があります。
以下のコマンドを実行し、マイグレーションを行います。

以下のコマンドは、docker-compose upの状態で行ってください。

docker-compose run backend python manage.py makemigrations
docker-compose run backend python manage.py migrate

次にSchemaを定義します。

from ninja import Schema

class UserSchema(Schema):
  id: int
  username: str

class TaskSchema(Schema):
  id: int
  title: str
  is_completed: bool
  owner: UserSchema = None

TaskSchemaのownerに先に設定したUserSchemaを結びつけいています。
これにより、TaskSchemaownerについては、UserSchemaを参照することができるようになり、参照しているUserSchemaの型が適用されます。

最後にAPIを作成します。

from ninja import NinjaAPI
from typing import List # add

from example.Schema.api_v4_schema import UserIn, UserOut, UserSchema, TaskSchema # add
from register.models import User

app_v4 = NinjaAPI(
  title = "Django-Ninja Sample App4",
  version = "1.0.4",
)

# add 2
@app_v4.get("/tasks", response = List[TaskSchema])
def tasks(request):
  queryset = Task.objects.select_related("owner")
  return list(queryset)

レスポンスとしてList形式でTaskSchemaに基づいたレスポンスが返されます。
また、select_relatedownerに関するデータも併せて取得するし、querysetに代入しています。

出力結果

自動生成ドキュメントを見てみます。

まずはSchemaです。

出力された自動生成ドキュメントのSchema

TaskSchemaに併せて、ownerについては、UserSchemaを参照し、その中身を返すようになっています。

また、APIも見てみましょう。

APIもownerを参照している

APIも自動生成され、しっかりとownerについてもUserSchemaを参照しています。

ちなみに、事前にhttp://127.0.0.1:8000/adminでDjangoのデータベースにタスクをいくつか登録してから参照すると、しっかりとList形式でタスクを参照できていることがわかります。

タスク登録後に実行したgetメソッド

aliasの設定

次に、aliasの設定を見ていきます。
aliasの設定をすることで、上記のようにネストされたレスポンスを返す代わりに、レスポンスの出力をシンプルにすることができます。
なお、Django NinjaのSchemaオブジェクトは、PydanticのField(..., alias="")形式を拡張します。

今回は、上記で作成したモデルを使用し、タスクのownerの名前をインラインで含むSchemaを作成し、すでに作成しているis_completedではなく、completedに置き換えます。

# changed
class TaskSchema(Schema):
  id: int
  title: str
  completed: bool = Field(..., alias = "is_completed")
  owner_username: str = Field(None, alias = "owner.username")

aliasの設定により、completedとして返される値の中身は、モデルで作成したis_completedとなります。
owner_usernameも同様の考え方です。

Resolversの設定

フィールド名に基づき、resolveメソッドを介して、計算フィールドを作成することもできます。
メソッドは、Schemaに対して提供するresolvingされたオブジェクトを単一の引数として受け入れる必要があります。
標準メソッドとしてのResolverを作成するselfと、スキーマ内の他のバリデーション済みであり、かつ、フォーマット済みの属性にアクセスすることができるようになります。

例を見てみます。

すでに作成しているTaskSchemaを修正します。

from ninja import Schema, Field
from typing import Optional # add

class UserSchema(Schema):
  id: int
  username: str

# changed
class TaskSchema(Schema):
  id: int
  title: str
  is_completed: bool
  owner: Optional[str]
  lower_title: str
  
  @staticmethod
  def resolve_owner(obj):
    if not obj.owner:
      return
    return f"{obj.owner.username}"
  
  def resolve_lower_title(self, obj):
    return self.title.lower()

上記のように修正することで、ownerstr型かNoneを返すようになります。
しかし、その下で@staticmethodを定義し、ownerの有無を確認しています。
また、@staticmethod内の最後のlower_titleについては、Djangoモデルとしてのlower_titleを設定している訳ではなく、このTaskSchemaに基づいて返されるレスポンスについて、小文字化したtitleを返す関数を設定しているものです。

自動生成ドキュメントで試してみると、結果は正しく返ってきます。

resolver設定後の自動生成ドキュメント

querysetを返す

前の例では、具体的にクエリセットをリストに変換してレスポンスとして返していました。
これを回避し、結果としてquerysetを返すことができます。
そして、このレスポンスは自動的にListとして判断されます。

初めに設定したAPIは、

@app_v4.get("/tasks", response = List[TaskSchema])
def tasks(request):
  queryset = Task.objects.select_related("owner")
  return list(queryset)

でしたが、以下のように変更することで、querysetを定義することなく、querysetList形式で返します。

@app_v4.get("/tasks", response = List[TaskSchema])
def tasks(request):
  return Task.objects.all()

結果は、先ほどと同様のレスポンスが返ってきていることが確認できるかと思います。

なお、非同期関数として定義する場合は、クエリを安全に呼び出す観点から、この例をasync defで書き換えただけでは機能しません。
詳細は、こちらの非同期サポートを確認してください。

FileField と ImageField

次に見ていくのは、FileFieldImageFieldについてです。

Django Ninjaにおいて、ファイルと画像(FileFieldまたはImageFieldで宣言する) をstring形式でURLに変換します。

まずはモデルを定義します。
Pictureモデルを作成し、マイグレートしていきます。

from django.db import models

class Picture(models.Model):
  title = models.CharField(max_length = 100)
  image = models.ImageField(upload_to = "images")
  
  def __str__(self):
    return self.title

また、アプリケーションディレクトリのadmin.pyでもPictureモデルを読み込ませます。

from django.contrib import admin

from .models import Department, Employee, Task, Picture # add

admin.site.register(Department)
admin.site.register(Employee)
admin.site.register(Task)
admin.site.register(Picture) # add

docker-compose upをした状態で、別のターミナルを開き、以下のコマンドを実行します。

docker-compose run backend python manage.py makemigrations 
docker-compose run backend python manage.py migrate

次のAPIの検証のため、Djangoのadminから、Pictureモデルに要素をいくつか追加しておいてください。

次にSchemaを作成します。
今回は、モデルで作成したPictureに対応する形でSchemaを設定していきます。

from ninja import Schema

class PictureSchema(Schema):
  title: str
  image: str

そして、APIを作成します。

from ninja import NinjaAPI
from typing import List

app_v4 = NinjaAPI(
  title = "Django-Ninja Sample App4",
  version = "1.0.4",
)

# add 3
@app_v4.get("/pictures", response = List[PictureSchema])
def pictures_list(request):
  return Picture.objects.all()

今回は、事前にDjangoのadminからアップロードしておいたファイルを参照するようにGETメソッドでAPIを設定しました。
(勉強中ということもあり、ファイルアップロードしながらできればと思いましたができませんでした。そのうち頑張りたいです。)

自動生成ドキュメントの結果を見てみます。

アップロード済みのファイルを参照した結果

このように結果が返ってくれば成功です。
事前に登録したファイルのtitleimageが返ってきます。
また、imageにはDjango Ninjaが自動でファイルアップロードをした先のURLが生成されます。

今回は、すでにアップロード済みのファイルを参照する形を取りましたが、アップロードしたファイルの結果を参照する際に使用することができると思いますので、今後アプリケーションを作っていく際に、取り組んでみたいと思います。

複数のレスポンスに対するSchemaの設定

アプリケーションによっては、レスポンスのSchema以上の内容を定義する必要が出てくることがあります。
たとえば、認証のAPIを作る場合、次のようなレスポンスを返すことができます。

  • 200 successful」の場合は「token
  • 401」の場合は、「Unauthorized
  • 402」の場合は「Payment required

実際、OpenAPIの仕様では、複数のレスポンスのSchemaを渡すことができます。
次のような辞書型のデータをresponseの引数に渡すことができます。

  • key ⇨ レスポンスコード
  • value ⇨ コードのSchema

また、レスポンスが返されるときは、ステータスコードを渡し、バリデーションとシリアル化に使用するスキーマをDjango Ninjaに送信する必要があります。

例を見てみます。

まずは、Schemaを定義します。

from ninja import Schema
from datetime import date

class Token(Schema):
  token: str
  expires: date

class Message(Schema):
  message: str

APIを定義します。

@api.post('/login', response={200: Token, 401: Message, 402: Message})
def login(request, payload: Auth):
    if auth_not_valid:
        return 401, {'message': 'Unauthorized'}
    if negative_balance:
        return 402, {'message': 'Insufficient balance amount. Please proceed to a payment page.'}
    return 200, {'token': xxx, ...}

このように渡すことができます。

【勉強不足です】
上記のAPIですが、payloadに渡してるAuthの取り扱いがわからず、ドキュメントのとおりの表記になってしまいました。
今後、勉強を進めていき、理解を深められた時に、詳しいところは更新したいと思います。
ご了承ください。

また、上記の例の場合、4xxのHttpステータスコードを書き、それに対するSchemaとしてMessageを複数回記述しています。
これに対応するため、Django Ninjaでは、下記のようなHttpコードを用意しています。

from ninja.responses import codes_1xx
from ninja.responses import codes_2xx
from ninja.responses import codes_3xx
from ninja.responses import codes_4xx
from ninja.responses import codes_5xx

これにより、上記のAPIを下記のように書き換えることができます。

from ninja.responses import codes_4xx

@api.post('/login', response={200: Token, codes_4xx: Message})
def login(request, payload: Auth):
    if auth_not_valid:
        return 401, {'message': 'Unauthorized'}
    if negative_balance:
        return 402, {'message': 'Insufficient balance amount. Please proceed to a payment page.'}
    return 200, {'token': xxx, ...}

このように、先頭が同じ番号で始まるステータスコードをまとめて提供しているため、これをインポートすることで、複数のステータスコードにまとめて同じSchemaを適用することができます。

また、このステータスコードについては、frozensetを用いることで、任意の範囲のステータスコードを指定し、独自のステータスコードの設定をすることもできます(以下に例を示します)。

my_codes = frozenset({416, 418, 425, 429, 451})
...
@api.post('/login', response={200: Token, my_codes: Message})
...

最後に

今回の記事は、Django Ninjaの具体的なSchemaの設定に関する内容でした。
モデルとの連携が多く盛り込まれており、Djangoとの連携を念頭に設計されていることがわかりました。

次回は、こちらのドキュメントを見ていきます。

今回のパートで取り扱ったドキュメントの中で、
・Multiple Response Schemas(ドキュメントのまま載せています)
・Multiple response codes
・Empty responses
・Self-referencing schemes
・Self-referencing schemes from create_schema()
・Serializing Outside of Views
については、説明を省略しました。
私の知識が追いついていないため、内容の理解ができなかったためです。
機会があれば、取り組んでいきたいと思いますので、その際は読んでいただければと思います。

Django-Ninjaチュートリアル⑩(レスポンススキーマ)

この記事が気に入ったら
フォローしてね!

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

はじめまして、ふじです。
Python、Django、FastAPI、React.js、Next.jsを学習している、ずっと文系のプログラミング独学者です。

目次