## 経緯
仕事で[[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を学習してみる]]