## 経緯 仕事で[[Django REST framework]]を使っているが、機能が多すぎて『何もわからん』という状態なので体系的に理解してみる。 ## 前提 [[Django]]のバージョンは4.2で進める。[[Django 4.2のサポートは2026年4月で切れる]]ので、5系のほうが良さそうに思えるが、以下の理由から4.2を選ぶ。 - 少なくとも数ヶ月は4.2を使い続ける可能性が高い - 5は使えない - 4.2を理解しておけば、5を学ぶときも差分学習で済むので遠回りにならない - 他の用途で[[Django]]や[[Django REST framework]]を使う予定はない ### 環境 | 対象 | バージョン | | ------------------------- | ------- | | [[macOS]] | 15.7 | | [[Ghostty]] | 1.2.0 | | [[VSCode]] | 1.104.1 | | [[Python]] | 3.13.7 | | [[Django]] | 4.2 | | [[Django REST framework]] | 3.16.1 | | [[Pip]] | 25.2 | | [[uv]] | 0.8.22 | | [[Ruff]] | 0.13.1 | | [[Docker]] | 28.5.1 | ## まずは動かしてみる [[GitHub]]のREADMEと、公式ドキュメントのHome、クイックスタート、全部書いてあることがバラバラでカオス。最新バージョンであればどれでも良さそうだが、バージョンfixなので慎重に。 ### [[ChatGPT]]に聞く > [!right-bubble] ![[minerva-face-right.webp]] > Django4.2でDjango REST frameworkを使いたいのですが、Django REST frameworkは最新バージョンでOK? > [!left-bubble] ![[chappy.webp]] > はい。Django REST framework (DRF) は Django 4.2 に対応しています。 > 最新の DRF 3.15 系をそのまま使って問題ありません。 > > - Django 4.2 は LTS > - DRF 3.15 は Django 3.2, 4.2, 5.0 系に対応 > > → `pip install djangorestframework` で最新を入れてOKです。 > > 確認: [公式リリースノート](https://www.django-rest-framework.org/community/release-notes/#3150) にも Django 4.2 サポートが明記されています。 > ### [3.15.2](https://www.django-rest-framework.org/community/release-notes/#3152) > > **Date**: 14th June 2024 > > - Fix potential XSS vulnerability in browsable API. [#9435](https://github.com/encode/django-rest-framework/pull/9435) > - Revert "Ensure CursorPagination respects nulls in the ordering field". [#9381](https://github.com/encode/django-rest-framework/pull/9381) > - Use warnings rather than logging a warning for DecimalField. [#9367](https://github.com/encode/django-rest-framework/pull/9367) > - Remove unused code. [#9393](https://github.com/encode/django-rest-framework/pull/9393) > - Django < 4.2 and Python < 3.8 no longer supported. [#9393](https://github.com/encode/django-rest-framework/pull/9393) > > *[Release Notes - Django REST framework](https://www.django-rest-framework.org/community/release-notes/#315x-series)* 4.2未満がアウトなので4.2は大丈夫そう。なお、[[Django REST framework]]の最新バージョンは3.16.1みたいだが、そこまでに4.2のサポートが切られたという記述はないので恐らく平気。 また、[[Django REST framework]]の `requirements.txt` を確認したところ、`django` の記載はなかったので明示的にインストールが必要そう。 ### [[Django]]の明示的インストールなしでやってみる ものは試しということでクイックスタートをやってみる。 <div class="link-card-v2"> <div class="link-card-v2-site"> <img class="link-card-v2-site-icon" src="https://www.django-rest-framework.org/img/favicon.ico" /> <span class="link-card-v2-site-name">www.django-rest-framework.org</span> </div> <div class="link-card-v2-title"> Quickstart - Django REST framework </div> <div class="link-card-v2-content"> Django, API, REST, Quickstart </div> <a href="https://www.django-rest-framework.org/tutorial/quickstart/"></a> </div> ```console mkdir drf-sandbox cd drf-sandbox python -m venv venv v ``` 早速問題が発生。 ``` $ uv pip install djangorestframework Using Python 3.13.7 environment at: venv Resolved 4 packages in 593ms Prepared 4 packages in 1.22s Installed 4 packages in 75ms + asgiref==3.10.0 + django==5.2.6 + djangorestframework==3.16.1 + sqlparse==0.5.3 ``` [[Django]]の5.2.6がインストールされてしまった...。どこかに依存関係があったのだろうか。。。 ### [[Django]]を明示的にインストールしてみる 先ほど作成したディレクトリは削除してリトライ。 ```console rm -rf drf-sandbox mkdir drf-sandbox cd drf-sandbox git init echo 'venv' >>.gitignore echo '*.pyc' >>.gitignore python -m venv venv v ``` ```console $ uv pip install django==4.2 Using Python 3.13.7 environment at: venv Resolved 3 packages in 222ms Prepared 1 package in 2.13s Installed 3 packages in 77ms + asgiref==3.10.0 + django==4.2 + sqlparse==0.5.3 ``` 第1関門はクリア。続けて[[Django REST framework]]をインストール。 ``` $ uv pip install djangorestframework Using Python 3.13.7 environment at: venv Resolved 4 packages in 4ms Installed 1 package in 4ms + djangorestframework==3.16.1 ``` よさそう。 ### プロジェクトのセットアップ プロジェクトを作成。 `tutorial` が本体。 ```console $ django-admin startproject tutorial . $ cd tutorial $ tree  . ├──  __init__.py ├──  asgi.py ├──  settings.py ├──  urls.py └──  wsgi.py ``` > [!attention] > 最後の `.` を忘れないように アプリの作成で `quickstart` が生える。 ``` $ django-admin startapp quickstart $ tree  . ├──  __init__.py ├──  asgi.py ├──  quickstart │ ├──  __init__.py │ ├──  admin.py │ ├──  apps.py │ ├──  migrations │ │ └──  __init__.py │ ├──  models.py │ ├──  tests.py │ └──  views.py ├──  settings.py ├──  urls.py └──  wsgi.py ``` ### マイグレーションの実行 ソースコードの定義とDBの状態を同期する。以下の記述からデフォルトでは[[SQLite]]が利用される。 `tutorial/settings.py` ```python DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', 'NAME': BASE_DIR / 'db.sqlite3', } } ``` 最終的には[[PostgreSQL]]を利用する予定だが、今は一旦このまま進める。 ```console $ cd .. $ python manage.py migrate Operations to perform: Apply all migrations: admin, auth, contenttypes, sessions Running migrations: Applying contenttypes.0001_initial... OK Applying auth.0001_initial... OK Applying admin.0001_initial... OK Applying admin.0002_logentry_remove_auto_add... OK Applying admin.0003_logentry_add_action_flag_choices... OK Applying contenttypes.0002_remove_content_type_name... OK Applying auth.0002_alter_permission_name_max_length... OK Applying auth.0003_alter_user_email_max_length... OK Applying auth.0004_alter_user_username_opts... OK Applying auth.0005_alter_user_last_login_null... OK Applying auth.0006_require_contenttypes_0002... OK Applying auth.0007_alter_validators_add_error_messages... OK Applying auth.0008_alter_user_username_max_length... OK Applying auth.0009_alter_user_last_name_max_length... OK Applying auth.0010_alter_group_name_max_length... OK Applying auth.0011_update_proxy_permissions... OK Applying auth.0012_alter_user_first_name_max_length... OK Applying sessions.0001_initial... OK ``` 色々と作成されるが、モデル定義などをした覚えはないので[[Django]]のデフォルトからプラグイン周りの何かだろうと思われる。 さて、DBファイル `db.sqlite3` が作成されている。 ```  . ├──  db.sqlite3 ├──  manage.py ├──  tutorial └──  venv ``` ### 最初のユーザーを作成 `admin` さんだと捻りがないので `toa` にする。 ```console python manage.py createsuperuser --username toa --email [email protected] ``` パスワードを適当に入力して完了。 #### データベースの中身確認 `auth_user` テーブルにユーザー情報が登録されている。 | Name | Value | 備考 | | ------------ | ---------------------------- | ------------------------ | | id | 1 | | | password | pbkdf2_sha256$600000$5Fx..== | [[SHA-256]] + [[PBKDF2]] | | last_login | | | | is_superuser | 1 | すべての管理権限があるか | | username | toa | | | last_name | | | | email | [email protected] | | | is_staff | 1 | 管理サイトに入れるか | | is_active | 1 | | | date_joined | 2025-09-24 12:45:19.942329 | | | first_name | | | ### [[Serializer (DRF)|Serializer]]の実装 [[Serializer (DRF)|Serializer]]を `tutorial/quickstart/serializers.py` として作成する。 ```python from django.contrib.auth.models import Group, User from rest_framework import serializers class UserSerializer(serializers.HyperlinkedModelSerializer): class Meta: model = User fields = ["url", "username", "email", "groups"] class GroupSerializer(serializers.HyperlinkedModelSerializer): class Meta: model = Group fields = ["url", "name"] ``` ### [[View (DRF)|View]]の実装 [[View (DRF)|View]] `tutorial/quickstart/views.py` を編集する。 ```python from django.contrib.auth.models import Group, User from rest_framework import permissions, viewsets from tutorial.quickstart.serializers import GroupSerializer, UserSerializer class UserViewSet(viewsets.ModelViewSet): """ API endpoint that allows users to be viewed or edited. """ queryset = User.objects.all().order_by("-date_joined") serializer_class = UserSerializer permission_classes = [permissions.IsAuthenticated] class GroupViewSet(viewsets.ModelViewSet): """ API endpoint that allows groups to be viewed or edited. """ queryset = Group.objects.all().order_by("name") serializer_class = GroupSerializer permission_classes = [permissions.IsAuthenticated] ``` ### [[URL (DRF)|URL]]の実装 [[URL (DRF)|URL]] `tutorial/urls.py` を編集する。 ```python from django.urls import include, path from rest_framework import routers from tutorial.quickstart import views router = routers.DefaultRouter() router.register(r"users", views.UserViewSet) router.register(r"groups", views.GroupViewSet) urlpatterns = [ path("", include(router.urls)), path("api-auth/", include("rest_framework.urls", namespace="rest_framework")), ] ``` ### 設定の追加 `tutorials/settings.py` の `INSTALLED_APPS` に [[Django REST framework]] を追加する。 ```diff INSTALLED_APPS = [ "django.contrib.admin", "django.contrib.auth", "django.contrib.contenttypes", "django.contrib.sessions", "django.contrib.messages", "django.contrib.staticfiles", + "rest_framework", ] ``` ### 起動する APIサーバーを起動する。 ```console python manage.py runserver ``` http://127.0.0.1:8000/users/ にアクセスすると403になるので、右上から `Log in` する。 ![[2025-09-28-13-56-23.avif|frame]] *こんな感じになればOK* ### ページングを有効にする 起動したまま `tutorial/settings.py` の末尾にページング有効化設定を追加する。 ```python # 末尾に追加 REST_FRAMEWORK = { "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination", "PAGE_SIZE": 10, } ``` レスポンスのrootがオブジェクトに変わっており、ページング情報も返却されるようになった。 ```python { "count": 1, "next": null, "previous": null, "results": [ { "url": "[http://127.0.0.1:8000/users/1/](http://127.0.0.1:8000/users/1/)", "username": "toa", "email": "[[email protected]](mailto:[email protected])", "groups": [] } ] } ``` ### 環境構築シェル 何度も試す場合に。 ```bash #!/bin/bash set -eux mkdir drf-sandbox cd drf-sandbox git init echo 'venv' >>.gitignore echo '*.pyc' >>.gitignore python -m venv venv source venv/bin/activate uv pip install django==4.2 djangorestframework django-admin startproject tutorial . cd tutorial django-admin startapp quickstart cd .. ``` ## [[Serializer (DRF)|Serializer]] その1 クイックスタートの次のチュートリアルが[[Serializer (DRF)|Serializer]]なのでやってみる。 <div class="link-card-v2"> <div class="link-card-v2-site"> <img class="link-card-v2-site-icon" src="https://www.django-rest-framework.org/img/favicon.ico" /> <span class="link-card-v2-site-name">www.django-rest-framework.org</span> </div> <div class="link-card-v2-title"> 1 - Serialization - Django REST framework </div> <div class="link-card-v2-content"> Django, API, REST, 1 - Serialization </div> <a href="https://www.django-rest-framework.org/tutorial/1-serialization/"></a> </div> ただし、以下の理由から題材は変える。 - 模写を防ぐことで理解度を上げるため - サンプルの題材が基礎を学ぶにはnoisy ### 新しくプロジェクトを作成する クイックスタートは別にプロジェクトを作成する必要がある。今後はスクリプトの形で展開していく。 `create-drf-serializer.sh` ```bash #!/bin/bash set -eux mkdir drf-serializer-sandbox cd drf-serializer-sandbox git init echo 'venv' >>.gitignore echo '*.pyc' >>.gitignore python -m venv venv source venv/bin/activate uv pip install django==4.2 djangorestframework django-admin startproject zoo . cd zoo django-admin startapp animals cd .. ``` ```console  zoo ├──  manage.py └──  zoo ├──  __init__.py ├──  animals │ ├──  __init__.py │ ├──  admin.py │ ├──  apps.py │ ├──  migrations │ │ └──  __init__.py │ ├──  models.py │ ├──  tests.py │ └──  views.py ├──  asgi.py ├──  settings.py ├──  urls.py └──  wsgi.py ``` `zoo/settings.py` ```diff INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', + 'rest_framework', + 'zoo.animals', ] ``` `zoo/animals/apps.py` ```diff from django.apps import AppConfig class AnimalsConfig(AppConfig): default_auto_field = 'django.db.models.BigAutoField' - name = 'animals' + name = 'zoo.animals' ``` ### modelの作成 `zoo/animals/models.py` を編集する。 ```python from django.db import models ANIMAL_KIND = { "dog": "犬", "cat": "猫", "owl": "フクロウ", "gorilla": "ゴリラ", } class Animal(models.Model): id = models.UUIDField(primary_key=True, editable=False) name = models.CharField(max_length=100, blank=True, default="") description = models.TextField() proper = models.BooleanField(default=False) kind = models.CharField(choices=ANIMAL_KIND, default="owl", max_length=32) created = models.DateTimeField(auto_now_add=True) class Meta: ordering = ["created"] ``` 型エラーが出た。 ```error Argument of type "Literal[False]" cannot be assigned to parameter "default" of type "type[NOT_PROVIDED]" in function "__init__"   Type "Literal[False]" is not assignable to type "type[NOT_PROVIDED]" [reportArgumentType] ``` #### 型エラーの抑制 [[django-stubs]]をインストールすることである程度はカバーできる。 ```console $ uv pip install django-stubs Using Python 3.13.7 environment at: venv Resolved 7 packages in 3ms Installed 4 packages in 10ms + django-stubs==5.2.7 + django-stubs-ext==5.2.7 + types-pyyaml==6.0.12.20250915 + typing-extensions==4.15.0 ``` > [!question] django-stubsのバージョンをDjangoにあわせなくてよいのか? > [Verion compatibility](https://github.com/typeddjango/django-stubs?tab=readme-ov-file#version-compatibility) を見ると 4.2.7 ではないと駄目なのでは... という気がするが、4.2.7にすると型エラーが残るので仕方なく。。。よくない。 #### 動作確認 マイグレーションする。 ```console python manage.py makemigrations animals ``` エラーが出た。 ```error SystemCheckError: System check identified some issues: ERRORS: animals.Animal.kind: (fields.E005) 'choices' must be an iterable containing (actual value, human readable name) tuples. ``` [Django 4.2](https://docs.djangoproject.com/ja/4.2/ref/models/fields/#choices)のリファレンスを見ると、[Django 5.2では存在したマップの記述](https://docs.djangoproject.com/ja/5.2/ref/models/fields/#choices) がない。というわけで `ANIMAL_KIND` を書き換える。 ```python from django.db import models ANIMAL_KIND = ( ("dog", "犬"), ("cat", "猫"), ("owl", "フクロウ"), ("gorilla", "ゴリラ"), ) class Animal(models.Model): id = models.UUIDField(primary_key=True, editable=False) name = models.CharField(max_length=100, blank=True, default="") description = models.TextField() proper = models.BooleanField(default=False) kind = models.CharField(choices=ANIMAL_KIND, default="owl", max_length=32) created = models.DateTimeField(auto_now_add=True) class Meta: ordering = ["created"] ``` LGTM。 ```console $ python manage.py makemigrations animals Migrations for 'animals': zoo/animals/migrations/0001_initial.py - Create model Animal ``` そのままmigrateする。 ```console $ python manage.py migrate animals Operations to perform: Apply all migrations: animals Running migrations: Applying animals.0001_initial... OK ``` #### DBの確認 [[DDL (SQL)|DDL]]は以下のようになっている。 ```sql CREATE TABLE "animals_animal" ( "id" char(32) NOT NULL PRIMARY KEY, "name" varchar(100) NOT NULL, "description" text NOT NULL, "proper" bool NOT NULL, "kind" varchar(32) NOT NULL, "created" datetime NOT NULL ); ``` ここで先程の `models.py` の定義を見てみる。 ```python class Animal(models.Model): id = models.UUIDField(primary_key=True, editable=False) name = models.CharField(max_length=100, blank=True, default="") description = models.TextField() proper = models.BooleanField(default=False) kind = models.CharField(choices=ANIMAL_KIND, default="owl", max_length=32) created = models.DateTimeField(auto_now_add=True) class Meta: ordering = ["created"] ``` この[[DDL (SQL)|DDL]]ではいくつか定義が足りないことが分かる。 ## [[PostgreSQL]]を使う [[Serializer (DRF)|Serializer]]を進めるのをやめ、DBを[[SQLite]]から[[PostgreSQL]]に切り替える。[[#マイグレーションの実行]] のときに以下のように述べたからだ。 > 最終的には[[PostgreSQL]]を利用する予定だが、今は一旦このまま進める。 ### PostgreSQLのコンテナ作成 [[Docker Compose]]で[[PostgreSQL]]コンテナを作成する。[[PostgreSQL]]は最新バージョンの18を使ってみる。もし新しすぎて不都合があったらバージョンを下げればよい。 `docker-compose.yml` ```yaml services: db: image: postgres:18 container_name: postgres-drf ports: - 15432:5432 volumes: - ./docker-entrypoint-initdb.d:/docker-entrypoint-initdb.d environment: POSTGRES_PASSWORD: password ``` 初期化用のファイルを作成する。 `docker-entrypoint-initdb.d/init.sql` ```sql CREATE DATABASE drfdb; \c drfdb; ``` ### 起動 ```console docker compose up -d ``` ### 接続確認 ```console docker exec -it postgres-drf psql --username postgres drfdb ``` [[データベースクライアント]]が起動したらOK。 ### プロジェクトのDB設定を変更する 公式ドキュメントで[[PostgreSQL]]の設定方法を確認する。 <div class="link-card-v2"> <div class="link-card-v2-site"> <img class="link-card-v2-site-icon" src="https://static.djangoproject.com/img/icon-touch.e4872c4da341.png" /> <span class="link-card-v2-site-name">Django Project</span> </div> <div class="link-card-v2-title"> Settings | Django documentation </div> <div class="link-card-v2-content"> The web framework for perfectionists with deadlines. </div> <img class="link-card-v2-image" src="https://static.djangoproject.com/img/logos/django-logo-negative.1d528e2cb5fb.png" /> <a href="https://docs.djangoproject.com/en/4.2/ref/settings/#databases"></a> </div> こんな感じらしい。 ```python DATABASES = { "default": { "ENGINE": "django.db.backends.postgresql", "NAME": "mydatabase", "USER": "mydatabaseuser", "PASSWORD": "mypassword", "HOST": "127.0.0.1", "PORT": "5432", } } ``` 設定を変更する。 `zoo/settings.py` ```python DATABASES = { "default": { "ENGINE": "django.db.backends.postgresql", "NAME": "drfdb", "USER": "postgres", "PASSWORD": "password", "HOST": "127.0.0.1", "PORT": "15432", } } ``` マイグレーションファイル自体は変更がないので `migrate` を行う。 ```console python manage.py migrate animals ``` エラーが出る。 ```error Traceback (most recent call last): File "/Users/tadashi-aikawa/work/drf-study/drf-serializer-sandbox/venv/lib/python3.13/site-packages/django/db/backends/postgresql/base.py", line 25, in <module> import psycopg as Database ModuleNotFoundError: No module named 'psycopg' During handling of the above exception, another exception occurred: Traceback (most recent call last): File "/Users/tadashi-aikawa/work/drf-study/drf-serializer-sandbox/venv/lib/python3.13/site-packages/django/db/backends/postgresql/base.py", line 27, in <module> import psycopg2 as Database ModuleNotFoundError: No module named 'psycopg2' ``` ### [[psycopg3]]のインストール エラーメッセージ通りに[[psycopg2]]をインストールして... みようと思ったが、[[psycopg2]]より[[psycopg3]]を使ったほうがいいらしい。 > Note > > The psycopg2 package is still widely used and actively maintained, but it is not expected to receive new features. > > Psycopg 3 is the evolution of psycopg2 and is where new features are being developed: if you are starting a new project you should probably start from 3! > > *[psycopg2 · PyPI](https://pypi.org/project/psycopg2/)* エラーのTracebackを見ても、先に `psycopg` をimportしようとしており、`pip install` のパッケージ名から[[psycopg3]]が先にimportされている気がするので試してみる。 ```console $ uv pip install "psycopg[binary,pool]" Using Python 3.13.7 environment at: venv Resolved 4 packages in 586ms Prepared 3 packages in 408ms Installed 3 packages in 6ms + psycopg==3.2.10 + psycopg-binary==3.2.10 + psycopg-pool==3.2.6 ``` 再び `migrate` を実行。 ```console $ python manage.py migrate animals Operations to perform: Apply all migrations: animals Running migrations: Applying animals.0001_initial... OK ``` 上手くいった。 ### DBの確認 ```sql CREATE TABLE public.animals_animal ( id uuid NOT NULL, name varchar(100) NOT NULL, description text NOT NULL, proper bool NOT NULL, kind varchar(32) NOT NULL, created timestamptz NOT NULL, CONSTRAINT animals_animal_pkey PRIMARY KEY (id) ); ``` あれ、あまり[[SQLite]]のときと変わっていない気が...。特に `default` 設定したのに `NOT NULL` になるけど、これはこういうものなのか。 どうやら、[[Django]]経由([[ORM]]経由)で操作したときに `default` を入れるよって意味で、DB直接の場合は別らしい。 [[PostgreSQL]]の導入が無駄になってしまったかもしれないが、どこかで入れる予定だったのでこれはこれでよし。 ## [[Serializer (DRF)|Serializer]] その2 続きをやる。 <div class="link-card-v2"> <div class="link-card-v2-site"> <img class="link-card-v2-site-icon" src="https://www.django-rest-framework.org/img/favicon.ico" /> <span class="link-card-v2-site-name">www.django-rest-framework.org</span> </div> <div class="link-card-v2-title"> 1 - Serialization - Django REST framework </div> <div class="link-card-v2-content"> Django, API, REST, 1 - Serialization </div> <a href="https://www.django-rest-framework.org/tutorial/1-serialization/#creating-a-serializer-class"></a> </div> ### Serializerの定義を作成 `zoo/animals/serializers.py` ```python from rest_framework import serializers from zoo.animals.models import ANIMAL_KIND, Animal class AnimalSerializer(serializers.Serializer): id = serializers.UUIDField(read_only=True) name = serializers.CharField(required=False, max_length=100, allow_blank=True) description = serializers.CharField() proper = serializers.BooleanField(required=False) kind = serializers.ChoiceField(choices=ANIMAL_KIND, default="owl") created = serializers.DateTimeField(read_only=True) def create(self, validated_data): return Animal.objects.create(**validated_data) def update(self, instance, validated_data): # readonly=True なフィールドは更新しない instance.name = validated_data.get("name", instance.name) instance.description = validated_data.get("description", instance.description) instance.proper = validated_data.get("proper", instance.proper) instance.kind = validated_data.get("kind", instance.kind) instance.save() return instance ``` ### Modelの修正 このままだと `id` を指定しなければエラーになってしまう。`models.py` で `id` のデフォルト値を指定する。 `zoo/animals/models.py` ```python from django.db import models from rest_framework.fields import uuid ANIMAL_KIND = ( ("dog", "犬"), ("cat", "猫"), ("owl", "フクロウ"), ("gorilla", "ゴリラ"), ) class Animal(models.Model): id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) name = models.CharField(max_length=100, blank=True, default="") description = models.TextField() proper = models.BooleanField(default=False) kind = models.CharField(choices=ANIMAL_KIND, default="owl", max_length=32) created = models.DateTimeField(auto_now_add=True) class Meta: ordering = ["created"] ``` ### Modelを使ってデータを挿入 ```console python manage.py shell ``` ```python from zoo.animals.models import Animal from zoo.animals.serializers import AnimalSerializer from rest_framework.renderers import JSONRenderer from rest_framework.parsers import JSONParser dog = Animal(name="マサハル", description="我が家の愛犬", kind="dog") dog.save() cat = Animal(name="みたらし", description="我が家の愛猫", kind="cat") cat.save() owl = Animal(name="みみぞう", description="我が家の愛梟") owl.save() ``` この時点でDBにはレコードが入っている。 ``` id |name |description |proper|kind|created | -----|-------|---------------|------|----|-----------------------------| 2b...|マサハル|我が家の愛犬 |false |dog |2025-10-10 22:46:46.846 +0900| 5e...|みたらし|我が家の愛猫 |false |cat |2025-10-10 22:48:21.628 +0900| 2f...|みみぞう|我が家の愛梟 |false |owl |2025-10-10 22:48:26.983 +0900| ``` ### serializeする まず `AnimalSerializer` インスタンスを生成する。 ```python dog_serializer = AnimalSerializer(dog) dog_serializer.data # {'id': '2b8efa79-066a-4709-a0cf-6b2115d69f63', 'name': 'マサハル', 'description': '我が家の愛犬', 'proper': False, 'kind': 'dog', 'created': '2025-10-10T13:46:46.846566Z'} ``` Redererの `render` にSerializerインスタンスの `data` を流し込むことでserializeされた値を得る。 ```python dog_content = JSONRenderer().render(dog_serializer.data) dog_content # b'{"id":"2b8efa79-066a-4709-a0cf-6b2115d69f63","name":"\xe3\x83\x9e\xe3\x82\xb5\xe3\x83\x8f\xe3\x83\xab","description":"\xe6\x88\x91\xe3\x81\x8c\xe5\xae\xb6\xe3\x81\xae\xe6\x84\x9b\xe7\x8a\xac","proper":false,"kind":"dog","created":"2025-10-10T13:46:46.846566Z"}' ``` ### deserializeする dictの状態からdeserializeも試してみる。 ```python >>> s1 = AnimalSerializer(data={'id': '2b8efa79-066a-4709-a0cf-6b2115d69f63', 'name': 'マサハル', 'description': '我が家の愛犬', 'proper': False, 'kind': 'dog', 'created': '2025-10-10T13:46:46.846566Z'}) >>> s1.is_valid() True >>> s1 = AnimalSerializer(data={'name': 'マサハル', 'description': '我が家の愛犬', 'proper': False, 'kind': 'dog', 'created': '2025-10-10T13:46:46.846566Z'}) >>> s1.is_valid() True >>> s1 = AnimalSerializer(data={'name': 'マサハル', 'description': '我が家の愛犬', 'proper': False, 'kind': 'dog'}) >>> s1.is_valid() True >>> s1.data {'name': 'マサハル', 'description': '我が家の愛犬', 'proper': False, 'kind': 'dog'} >>> s1 = AnimalSerializer(data={'name': 'マサハル', 'description': '我が家の愛犬', 'proper': False}) >>> s1.is_valid() True >>> s1.data {'name': 'マサハル', 'description': '我が家の愛犬', 'proper': False, 'kind': 'owl'} >>> s1 = AnimalSerializer(data={'name': 'マサハル', 'proper': False}) >>> s1.is_valid() False ``` `read_only=True` の部分や `required=False` の部分は省略しても平気そうだが、そうでないところ(`description`)を省略すると `is_valid()` が `False` になるっぽい。 `default` の値は当然補填されるが、`kind` の `default` はmodelでも定義しているのでserializerになくてもいい気はする。 もちろん選択肢に含まれない値を指定しても、`is_valid()` は `False` になる。 ```python >>> s1 = AnimalSerializer(data={'name': 'マサハル', 'description': 'hoge', 'kind': 'bear'}) >>> s1.is_valid() False ``` ### DBに登録する ```python >>> s1 = AnimalSerializer(data={'name': 'マサハル', 'description': 'hoge', 'kind': 'bear'}) >>> s1.is_valid() False >>> s1.save() ``` `is_valid()` が `False` の場合は流石に止めてくれる。 ```error AssertionError: You cannot call `.save()` on a serializer with invalid data. ``` validならもちろんOK。 ```python >>> s1 = AnimalSerializer(data={'name': 'マサル', 'description': 'マサハルの子供', 'proper': False, 'kind': 'dog'}) >>> s1.is_valid() True >>> s1.save() <Animal: Animal object (111685d0-5cb5-4f10-a67e-6ae574ced35b)> ``` レコードが挿入されている。 ``` id |name |description |proper|kind|created | -----|-------|---------------|------|----|-----------------------------| 2b...|マサハル|我が家の愛犬 |false |dog |2025-10-10 22:46:46.846 +0900| 5e...|みたらし|我が家の愛猫 |false |cat |2025-10-10 22:48:21.628 +0900| 2f...|みみぞう|我が家の愛梟 |false |owl |2025-10-10 22:48:26.983 +0900| 11...|マサル |マサハルの子供 |false |dog |2025-10-10 23:11:43.456 +0900| ``` ## Serializer その3 ### [[ModelSerializer (DRF)|ModelSerializer]]を使う 変更前。 `zoo/animals/serializers.py` ```python from rest_framework import serializers from zoo.animals.models import ANIMAL_KIND, Animal class AnimalSerializer(serializers.Serializer): id = serializers.UUIDField(read_only=True) name = serializers.CharField(required=False, max_length=100, allow_blank=True) description = serializers.CharField() proper = serializers.BooleanField(required=False) kind = serializers.ChoiceField(choices=ANIMAL_KIND, default="owl") created = serializers.DateTimeField(read_only=True) def create(self, validated_data): return Animal.objects.create(**validated_data) def update(self, instance, validated_data): # readonly=True なフィールドは更新しない instance.name = validated_data.get("name", instance.name) instance.description = validated_data.get("description", instance.description) instance.proper = validated_data.get("proper", instance.proper) instance.kind = validated_data.get("kind", instance.kind) instance.save() return instance ``` 変更後。 `zoo/animals/serializers.py` ```python from rest_framework import serializers from zoo.animals.models import Animal class AnimalSerializer(serializers.ModelSerializer): class Meta: model = Animal fields = "__all__" ``` ## APIのエンドポイント作成 [[View (DRF)|View]]と[[URL (DRF)|URL]]を作成する。 <div class="link-card-v2"> <div class="link-card-v2-site"> <img class="link-card-v2-site-icon" src="https://www.django-rest-framework.org/img/favicon.ico" /> <span class="link-card-v2-site-name">www.django-rest-framework.org</span> </div> <div class="link-card-v2-title"> 1 - Serialization - Django REST framework </div> <div class="link-card-v2-content"> Django, API, REST, 1 - Serialization </div> <a href="https://www.django-rest-framework.org/tutorial/1-serialization/#writing-regular-django-views-using-our-serializer"></a> </div> ### コードの変更 `zoo/animals/views.py` ```python from django.http import HttpResponse, JsonResponse from django.views.decorators.csrf import csrf_exempt from rest_framework.parsers import JSONParser from zoo.animals.models import Animal from zoo.animals.serializers import AnimalSerializer @csrf_exempt def animal_list(request): if request.method == "GET": animals = Animal.objects.all() serializer = AnimalSerializer(animals, many=True) return JsonResponse(serializer.data, safe=False) elif request.method == "POST": data = JSONParser().parse(request) serializer = AnimalSerializer(data=data) if not serializer.is_valid(): return JsonResponse(serializer.errors, status=400) serializer.save() return JsonResponse(serializer.data, status=201) else: return HttpResponse(status=405) # Method Not Allowed @csrf_exempt def animal_detail(request, pk): try: animal = Animal.objects.get(pk=pk) except Animal.DoesNotExist: return HttpResponse(status=404) if request.method == "GET": serializer = AnimalSerializer(animal) return JsonResponse(serializer.data) elif request.method == "PUT": data = JSONParser().parse(request) serializer = AnimalSerializer(animal, data=data) if not serializer.is_valid(): return JsonResponse(serializer.errors, status=400) serializer.save() return JsonResponse(serializer.data) elif request.method == "DELETE": animal.delete() return HttpResponse(status=204) else: return HttpResponse(status=405) # Method Not Allowed ``` 最後に[[URL (DRF)|URL]]を作成。 `zoo/animals/urls.py` ```python from django.urls import path from zoo.animals import views urlpatterns = [ path("animals/", views.animal_list), path("animals/<uuid:pk>/", views.animal_detail), ] ``` `zoo/urls.py` ```python from django.urls import include, path urlpatterns = [ path("", include("zoo.animals.urls")), ] ``` ### サーバー起動 ```console python manage.py runserver ``` ### [[Bruno]]プロジェクトの作成 [[Bruno]]を使って動作確認をするのでプロジェクトを作成。 ```  e2e ├──  01_animalsの取得.bru ├──  02_animalsの登録.bru ├──  03_animalsの更新.bru ├──  04_animalsの削除.bru └──  bruno.json ``` `bruno.json` ```json { "version": "1", "name": "drf-study", "type": "collection", "ignore": [ "node_modules", ".git" ] } ``` `01_animalsの取得.bru ``` meta { name: 01_animalsの取得 type: http seq: 1 } get { url: http://localhost:8000/animals/ body: none auth: inherit } settings { encodeUrl: true } ``` `02_animalsの登録.bru ``` meta { name: 02_animalsの登録 type: http seq: 2 } post { url: http://localhost:8000/animals/ body: json auth: inherit } body:json { { "name": "みみこ", "description": "みみぞうの妹", "proper": false, "kind": "owl" } } vars:post-response { animal_id: res.body.id } settings { encodeUrl: true } ``` `03_animalsの更新.bru ``` meta { name: 03_animalsの更新 type: http seq: 3 } put { url: http://localhost:8000/animals/{{animal_id}}/ body: json auth: inherit } body:json { { "name": "みみたろう", "description": "みみこの妹", "proper": false, "kind": "owl" } } settings { encodeUrl: true } ``` `04_animalsの削除.bru ``` meta { name: 04_animalsの削除 type: http seq: 4 } delete { url: http://localhost:8000/animals/{{animal_id}}/ body: json auth: inherit } settings { encodeUrl: true } ``` 上から順番に実行したが期待通り動作した。 ## まとめ 長くなってきたのでここまでにする。最終的なプロジェクト作成用のスクリプトは以下。 `create-drf-serializer.sh` ```bash #!/bin/bash set -eux mkdir drf-serializer-sandbox cd drf-serializer-sandbox git init echo 'venv' >>.gitignore echo '*.pyc' >>.gitignore python -m venv venv source venv/bin/activate uv pip install django==4.2 djangorestframework "psycopg[binary,pool]" django-stubs django-admin startproject zoo . cd zoo django-admin startapp animals cd .. ``` DB作成関連。 `docker-compose.yml` ```yaml services: db: image: postgres:18 container_name: postgres-drf ports: - 15432:5432 volumes: - ./docker-entrypoint-initdb.d:/docker-entrypoint-initdb.d environment: POSTGRES_PASSWORD: password ``` `docker-entrypoint-initdb.d/init.sql` ```sql CREATE DATABASE drfdb; \c drfdb; ``` ### リポジトリ 歴史を遡る可能性があるのでリポジトリにしておく。 <div class="link-card-v2"> <div class="link-card-v2-site"> <img class="link-card-v2-site-icon" src="https://github.githubassets.com/favicons/favicon.svg" /> <span class="link-card-v2-site-name">GitHub</span> </div> <div class="link-card-v2-title"> GitHub - tadashi-aikawa/drf-study </div> <div class="link-card-v2-content"> Contribute to tadashi-aikawa/drf-study development by creating an account on GitHub. </div> <img class="link-card-v2-image" src="https://opengraph.githubassets.com/c260bdc5d79358b4b08371606f330a030b07c6b5a07bde392970b7a22edf21a8/tadashi-aikawa/drf-study" /> <a href="https://github.com/tadashi-aikawa/drf-study"></a> </div> ### 続き > [[📜2025-10-11 Django REST frameworkを学習してみる]]