---
title: "Django REST FrameworkのSerializer（シリアライザ）とは？使い方・検証・整形・ネストをDRF 3.18で実行して解説"
url: "https://www.issoh.co.jp/tech/details/3276/"
published: 2024-08-05
updated: 2026-10-28
categories: ["Django"]
publisher: "株式会社一創"
---

# Django REST FrameworkのSerializer（シリアライザ）とは？使い方・検証・整形・ネストをDRF 3.18で実行して解説

Django REST Framework（DRF）のSerializer（シリアライザ）は、Djangoのモデルインスタンスやクエリセットを、JSONにしやすいPythonの辞書・リストへ変換する部品です。逆向きでは、パーサーが解析したリクエストのデータを検証し、モデルへ保存できる値に戻します。JSON文字列との変換そのものはレンダラーとパーサーの担当です。本記事のコードと出力は、DRF 3.18.3・Django 6.1.2・Python 3.13で実行した検証コードから抜粋しています（説明のためにクラス名や表示を整えた箇所があります）。

## まとめ：DRF Serializerの要点

- **Serializerは双方向の変換器**です。`serializer.data`で出力し、`data=`で受け取った値を`is_valid()`で検証してから`save()`します。
- **モデルのCRUDは`ModelSerializer`**で書きます。`fields`か`exclude`の指定は必須で、Userに`'__all__'`を使うとパスワードのハッシュまで出力されます。
- **検証は`validate_<field>()`が先、`validate()`が後**です。フィールド単位でエラーが出ると`validate()`は呼ばれません。
- **Userを既定の`create()`で保存するとパスワードが平文で入ります**。`create_user()`を呼ぶよう上書きします。
- **出力の整形**は`DateTimeField(format=...)`や`to_representation()`で行います。DBから読んだUTCの日時を`to_representation()`で直接`strftime`すると、UTCの日付が出ます。
- **ネストの一覧は対策なしで100件201クエリ**でした。`select_related`と`prefetch_related`で2クエリになります。
- **DRF 3.18.0（2026年8月）で`many=True`の検証エラーが辞書形式に変わりました**。旧形式は3.20で削除予定です。

## Serializer（シリアライザ）の役割：シリアライズとデシリアライズ

SerializerはDRFの中で「モデルやクエリセット」と「JSON」をつなぐ層です。ビュー・ルーター・認証などDRF全体の構成は[Django REST Framework（DRF）とは？Djangoで最短にREST APIを作る仕組みと使いどころ](/tech/details/3277/)で扱い、本記事はSerializerに絞ります。

### 2方向の変換と入口のAPI

変換の向きによって、Serializerへの渡し方と読む属性が変わります。

| 方向            | 呼び名     | 渡し方                        | 結果の取り出し                                 |
| ------------- | ------- | -------------------------- | --------------------------------------- |
| モデル→辞書（→JSON） | シリアライズ  | `PostSerializer(instance)` | `.data`                                 |
| 受信データ（辞書）→モデル | デシリアライズ | `PostSerializer(data=...)` | `is_valid()`→`.validated_data`→`save()` |

クエリセットを配列で出すときは`many=True`を付けます。デシリアライズでは`is_valid()`より前に`.data`や`.validated_data`を読むと`AssertionError`になります（実行時の文言は「You must call \`.is\_valid()\` before accessing \`.validated\_data\`.」）。

### SerializerとModelSerializerの使い分け

`ModelSerializer`は`Serializer`のサブクラスで、モデルの定義からフィールドと検証と`create()`/`update()`を組み立てます。

| 比較軸           | Serializer  | ModelSerializer      |
| ------------- | ----------- | -------------------- |
| フィールド定義       | すべて手書き      | モデルから自動生成            |
| create/update | 自分で実装       | 自動（上書き可）             |
| 一意制約の検証       | 自分で付ける      | UniqueValidatorを自動付与 |
| 向く入出力         | ログイン入力・集計結果 | 1モデルのCRUD            |

1つのモデルを読み書きするAPIなら`ModelSerializer`を選びます。素の`Serializer`は、ログインのユーザー名とパスワードのように保存先のモデルが無い入力、複数モデルを集計したレスポンス、モデルの構造とAPIの入出力を切り離して設計したい場合に使います。

## DRF 3.18.3のインストールと対応バージョン

2026年10月28日時点のPyPIの最新は[djangorestframework 3.18.3](https://pypi.org/project/djangorestframework/)（2026年10月6日公開）で、Python 3.10以上、Django 5.2・6.0・6.1に対応します。[リリースノート](https://www.django-rest-framework.org/community/release-notes/)によると3.18.0でDjango 4.2・5.0・5.1のサポートを外しています。Django 4.2はLTSのサポート期間が2026年4月に終わっているため、DRFを3.17系に据え置くより、Django 5.2 LTS以降へ上げるのが先です。Django 6.1自体はPython 3.12以上が必要です。

```
pip install "djangorestframework==3.18.3" "django==6.1.2"
```

```
# settings.py
INSTALLED_APPS = [
    # ...
    "rest_framework",
]
```

## ModelSerializerの定義とレスポンスへの受け渡し

以降の例は次の4モデルを使います。設定は`USE_TZ = True`・`TIME_ZONE = "Asia/Tokyo"`・`LANGUAGE_CODE = "ja"`、DBはSQLiteです。

```
class Author(models.Model):
    name = models.CharField(max_length=50)

class Tag(models.Model):
    name = models.CharField(max_length=30, unique=True)

class Post(models.Model):
    title = models.CharField(max_length=100)
    body = models.TextField(blank=True)
    price = models.IntegerField(default=0)
    author = models.ForeignKey(Author, on_delete=models.CASCADE, related_name="posts")
    tags = models.ManyToManyField(Tag, blank=True)
    created = models.DateTimeField(auto_now_add=True)

class Comment(models.Model):
    post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name="comments")
    text = models.CharField(max_length=100)
```

### fieldsの指定と自動生成されるフィールド

`Meta`に`model`と`fields`を書きます。`print(repr(...))`でModelSerializerが何を組み立てたかを確認できます。

```
# serializers.py
from rest_framework import serializers
from .models import Post

class PostSerializer(serializers.ModelSerializer):
    class Meta:
        model = Post
        fields = ["id", "title", "price", "author", "tags", "created"]

>>> print(repr(PostSerializer()))
PostSerializer():
    id = BigIntegerField(label='ID', read_only=True)
    title = CharField(max_length=100)
    price = IntegerField(max_value=9223372036854775807, min_value=-9223372036854775808, required=False)
    author = PrimaryKeyRelatedField(queryset=Author.objects.all())
    tags = PrimaryKeyRelatedField(many=True, queryset=Tag.objects.all(), required=False)
    created = DateTimeField(read_only=True)
```

読み取れることは3つあります。`auto_now_add`の列と主キーは`read_only=True`になる、`default`や`blank`のある列は`required=False`になる、ForeignKeyとManyToManyFieldは既定でIDの出し入れになる、の3点です。`id`が`BigIntegerField`になっているのは、[Django 6.0でDEFAULT\_AUTO\_FIELDの既定がBigAutoFieldに変わった](https://docs.djangoproject.com/en/6.1/releases/6.0/)ためです。JavaScriptで253を超える値を扱うなら、DRF 3.17.0で入った`COERCE_BIGINT_TO_STRING`設定で文字列として返せます。

`fields`と`exclude`のどちらも書かないと、クラスを使った時点で次のエラーになります。

```
AssertionError: Creating a ModelSerializer without either the 'fields' attribute or the 'exclude' attribute has been deprecated since 3.3.0, and is now disallowed. Add an explicit fields = '__all__' to the Bad serializer.
```

エラー文は`'__all__'`を勧めますが、Userモデルに`fields = "__all__"`を付けて出力すると、`password`（`pbkdf2_sha256$...`で始まるハッシュ）・`is_superuser`・`user_permissions`まで含む13項目がそのままレスポンスに出ました。公開するフィールドは列挙で書きます。

### ビューでのResponseへの受け渡し

ビューでは`.data`を`Response`に渡すだけです。JSONへの変換はレンダラーが行います。

```
from rest_framework.views import APIView
from rest_framework.response import Response
from .models import Post
from .serializers import PostSerializer

class PostList(APIView):
    def get(self, request):
        posts = Post.objects.all()
        return Response(PostSerializer(posts, many=True).data)
```

## フィールドオプションによる入出力の制御

フィールドは「値の型」と「どちらの方向で使うか」の2つで決まります。型はModelSerializerがモデルから推論するので、方向と参照元をオプションで指定するのが主な作業です。

| オプション                | 出力        | 入力       | 典型例           |
| -------------------- | --------- | -------- | ------------- |
| read\_only=True      | 含める       | 無視       | id・作成日時       |
| write\_only=True     | 出さない      | 受け取る     | パスワード         |
| required=False       | 属性があれば含める | 省略可      | 任意項目          |
| source=”author.name” | 参照先の値     | 入れ子で受け取る | API名とモデル名を変える |

```
class UserSerializer(serializers.ModelSerializer):
    joined = serializers.DateTimeField(source="date_joined", read_only=True)

    class Meta:
        model = User
        fields = ["id", "username", "password", "joined"]
        extra_kwargs = {"password": {"write_only": True}}
```

モデル由来のフィールドに`write_only`だけを足すなら、フィールドを宣言し直さず`extra_kwargs`に書けます。`source`にはドット区切りで関連先の属性も指定でき、`source="author.name"`と書けば著者名を平らな項目として出せます。入力側では`validated_data`に`{"author": {"name": ...}}`の入れ子で入るだけで、関連先への保存は自分で書きます。

## バリデーションの書き方と実行順

デシリアライズの検証は、フィールドごとの型変換と制約・`validate_<フィールド名>()`、`Meta.validators`などSerializer全体のバリデータ、最後に`validate()`の順で動きます（DRFの`run_validation()`の実装順）。

### validate\_<field>()とvalidate()の実行順

次のSerializerの各メソッドに呼び出しを記録する`print`を入れて実行しました（掲載コードでは`print`を省いています）。

```
class PeriodSerializer(serializers.Serializer):
    start = serializers.IntegerField()
    end = serializers.IntegerField()

    def validate_start(self, value):
        if value < 0:
            raise serializers.ValidationError("負数不可")
        return value

    def validate(self, attrs):
        if attrs["start"] > attrs["end"]:
            raise serializers.ValidationError("startはend以下")
        return attrs

# start=-1, end="x" のとき
validate_start called
False {'start': [ErrorDetail(string='負数不可', code='invalid')],
       'end': [ErrorDetail(string='有効な整数を入力してください。', code='invalid')]}

# start=5, end=1 のとき
validate_start called
validate called
False {'non_field_errors': [ErrorDetail(string='startはend以下', code='invalid')]}
```

フィールド単位のエラーが1つでもあると`validate()`は実行されません。全体のバリデータ（`UniqueTogetherValidator`など）が失敗した場合も同じです。複数フィールドをまたぐ検証は`validate()`に書き、その時点で`attrs`の各値は型変換済みだと前提にできます。`validate()`で文字列のエラーを出すと、既定では`non_field_errors`キーに入ります（キー名は`NON_FIELD_ERRORS_KEY`設定で変更でき、`{"end": "..."}`のように辞書で出せば項目別のエラーになります）。エラー文が日本語なのは`LANGUAGE_CODE = "ja"`のときで、DRFの翻訳が使われます。どこまでをSerializerで弾くかの設計は[入力バリデーションとは？防げる攻撃の範囲と許可リスト設計を実装目線で解説](/tech/details/16364/)で扱っています。

### 一意制約に対するUniqueValidatorの自動付与

モデル側で`unique=True`にした列には、ModelSerializerが`UniqueValidator`を付けます。`repr`では`name = CharField(max_length=30, validators=[<UniqueValidator(queryset=Tag.objects.all())>])`と表示され、既存と同じ名前を送ると「この name を持った tag が既に存在します。」が返りました。自分で`validate_name()`に重複チェックを書く必要はありません。ただしこれは保存前の事前確認で、同じ名前が同時に2件送られた場合に一意性を保証するのはDB側の一意制約です。

### DRF 3.18.0でのmany=Trueのエラー形式の変更

配列をまとめて検証するとき、エラーの形がDRF 3.18.0（2026年8月7日）で変わりました。3件中2件目だけが不正なデータを送った結果です。

```
# 3.18 の既定
{"1":{"name":["この項目は空にできません。"]}}

# LIST_SERIALIZER_ERRORS_AS_DICT=False（3.17 以前と同じ形）
[{}, {'name': [ErrorDetail(string='この項目は空にできません。', code='blank')]}, {}]
# RemovedInDRF320Warning が出る
```

新しい形式は不正な要素の添字をキーにした辞書で、正しい要素は含みません。フロントエンドが「配列のn番目」でエラーを拾っている場合は、更新後に表示が崩れます。[公式ドキュメント](https://www.django-rest-framework.org/api-guide/serializers/)は、3.18.1で追加された`REST_FRAMEWORK["LIST_SERIALIZER_ERRORS_AS_DICT"] = False`で旧形式に戻せるとしていますが、旧形式は3.20で削除予定です。設定で戻すのは移行期間だけにし、クライアント側を辞書形式に合わせます。

## is\_valid()からsave()まで：部分更新とパスワードの扱い

ビューでは`is_valid()`で検証し、通ったら`save()`します。`raise_exception=True`を付けると、不正なときに例外が出て、DRFの例外ハンドラーが400を返します。

```
class PostCreateSerializer(serializers.ModelSerializer):
    class Meta:
        model = Post
        fields = ["id", "title", "price", "tags"]   # author は入力させない

serializer = PostCreateSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
serializer.save(author=author)   # ビュー側で決めた著者を渡す
return Response(serializer.data, status=201)
```

`save()`にキーワード引数を渡すと、`validated_data`に足してから`create()`/`update()`が呼ばれます。リクエストに含めさせたくない値（ログイン中の利用者にひもづく著者など）は、`fields`から外したうえでこの形で渡します。

### PATCHでのpartial=Trueによる必須項目の省略

既存の投稿の`price`だけを変える場合、`partial=True`の有無で結果が分かれました。

```
PostSerializer(post, data={"price": 200}, partial=True).is_valid()
# True
PostSerializer(post, data={"price": 200}).is_valid()
# False {'title': ['この項目は必須です。'], 'author': ['この項目は必須です。']}
```

ModelViewSetの`partial_update`（PATCH）は内部で`partial=True`を渡しています。自前のビューでPATCHを受けるときは自分で付けます。

### Userの既定create()によるパスワードの平文保存

`password`を`write_only`にしたModelSerializerで利用者を登録すると、既定の`create()`は`User.objects.create(**validated_data)`を呼びます。実行すると、DBの`password`列に送った文字列がそのまま入り、`check_password()`は`False`でした。この利用者はログインできません。

```
stored= Secr3t!pass check= False          # 既定の create()
stored2= pbkdf2_sha256$1500000$... check= True   # create_user() に替えた後
```

```
class SignupSerializer(serializers.ModelSerializer):
    class Meta:
        model = User
        fields = ["username", "password"]
        extra_kwargs = {"password": {"write_only": True}}

    def create(self, validated_data):
        # create_user() は内部で set_password() を呼びハッシュ化する
        return User.objects.create_user(**validated_data)
```

更新側の`update()`も同じで、パスワード変更を受けるなら`instance.set_password()`を呼ぶ処理を書きます。なお`create_user()`が行うのはハッシュ化だけで、`AUTH_PASSWORD_VALIDATORS`の強度チェックは走りません。登録APIでは`validate_password()`（`django.contrib.auth.password_validation`）で検証し、DjangoのValidationErrorをDRFのフィールドエラーに変換して返します。

## 認証とSerializer：ログイン入力の検証と利用者の受け渡し

「Serializerの認証方式」を探している場合、まず分けておくことがあります。リクエストが誰からかを判定するのはDRFの認証クラス（`SessionAuthentication`・`TokenAuthentication`や、JWTのライブラリ）で、Serializerではありません。Serializerが関わるのは「ログインで送られたユーザー名とパスワードの検証」と「認証済みの利用者を保存データに入れること」の2か所です。JWTを含む構成は[ReactとDjangoの連携方法｜DRF・CORS・JWT認証の実装手順と本番構成](/tech/details/4421/)で扱っています。

### ログイン入力に対するvalidate()内の認証処理

DRF同梱の`rest_framework.authtoken.serializers.AuthTokenSerializer`が手本になります。`validate()`の中でDjangoの`authenticate()`を呼び、成功したら`attrs["user"]`に利用者を入れる作りです。誤ったパスワードと正しいパスワードで試しました。

```
from rest_framework.authtoken.serializers import AuthTokenSerializer

s = AuthTokenSerializer(data={"username": "bob", "password": "wrong"})
s.is_valid()
# False {'non_field_errors': [ErrorDetail(string='提供された認証情報でログインできません。', code='authorization')]}

s = AuthTokenSerializer(data={"username": "bob", "password": "right-pass"})
s.is_valid()
# True  validated_data に 'user': <User: bob> が入る
```

ユーザー名の有無とパスワードの誤りを区別しない1つのメッセージにしているのは、存在するアカウント名を推測させないためです。独自のログインSerializerを書くときもこの形を踏襲します。

### HiddenFieldとCurrentUserDefaultによる作成者の設定

新規作成時に投稿者をログイン中の利用者にしたい場合、`HiddenField(default=CurrentUserDefault())`を使うとリクエストの値を無視して`request.user`が入ります。`partial=True`（PATCH）では`default`が使われず、`validated_data`に`user`は入りませんでした。更新者を記録するならビューで`save(updated_by=request.user)`のように渡します。ただしSerializerの`context`に`request`が無いと`KeyError: 'request'`で止まりました。GenericAPIViewやViewSetは`get_serializer()`で自動的に渡しますが、テストやAPIViewで直接作るときは`context={"request": request}`を付けます。

```
class CommentSerializer(serializers.Serializer):
    user = serializers.HiddenField(default=serializers.CurrentUserDefault())
    text = serializers.CharField()

# data に "user": 999 を混ぜても無視され、request.user が入る
s = CommentSerializer(data={"text": "hi", "user": 999}, context={"request": request})
s.is_valid()  # True  {'user': <User: alice>, 'text': 'hi'}
```

## レスポンスを整形する：日時・小数・to\_representation

出力の見た目を変える手段は、変更の範囲で選びます。全APIで揃えるなら設定、1項目ならフィールドの引数、構造ごと変えるなら`to_representation()`です。

### フィールド引数による日時・小数の出力設定

`DateTimeField`は既定でISO 8601を返し、`USE_TZ = True`・`TIME_ZONE = "Asia/Tokyo"`の設定では`2026-10-28T02:29:05.301151+09:00`のように出ました。表示用の書式にするなら`format`引数を使います。`DecimalField`は既定で文字列（`'1000.00'`）を返し、`coerce_to_string=False`で数値として出せます。

```
class PostSerializer(serializers.ModelSerializer):
    created = serializers.DateTimeField(format="%Y-%m-%d %H:%M", read_only=True)
    # 出力: '2026-10-28 01:30'（UTC 16:30 の値を日本時間で出している）
```

全APIで統一するなら、`REST_FRAMEWORK`設定の`DATETIME_FORMAT`と`COERCE_DECIMAL_TO_STRING`で既定を変えます。

### to\_representation()による出力構造の変更

キーの追加や空値の削除のように構造そのものを変えるときは、`to_representation()`を上書きします。親の結果を受け取って手を加える形にすると、フィールド定義を活かせます。

```
from django.utils import timezone

class PostSerializer(serializers.ModelSerializer):
    author_name = serializers.CharField(source="author.name", read_only=True)

    class Meta:
        model = Post
        fields = ["id", "title", "body", "price", "author_name", "created"]

    def to_representation(self, instance):
        data = super().to_representation(instance)
        data["price_label"] = f"{instance.price:,}円"
        data["created"] = timezone.localtime(instance.created).strftime("%Y-%m-%d")
        # 空文字と None の項目を落とす
        return {k: v for k, v in data.items() if v not in (None, "")}

# {'id': 1, 'title': 'p0', 'price': 1000, 'author_name': '山田',
#  'created': '2026-10-28', 'price_label': '1,000円'}
```

ここで`timezone.localtime()`を省いて`instance.created.strftime()`と書くと、`USE_TZ = True`でDBから読んだUTCの値がそのまま使われます。日本時間で10月28日1時30分の投稿が、`localtime`なしでは「2026-10-27」と出ました。`DateTimeField`はタイムゾーンを変換してくれますが、`to_representation()`の中で生の値を触るとこの変換を通りません。

### SerializerMethodFieldを使う場面と避ける場面

モデルに無い値を出すときは`SerializerMethodField`を使います。既定で`get_<フィールド名>(self, obj)`が呼ばれ、戻り値が出力されます。

```
class PostSerializer(serializers.ModelSerializer):
    comment_count = serializers.SerializerMethodField()

    class Meta:
        model = Post
        fields = ["id", "title", "comment_count"]

    def get_comment_count(self, obj):
        return obj.comments.count()
```

使わないほうがよい場面は3つです。1つ目は入力も受けたい値で、`SerializerMethodField`は常に読み取り専用です。2つ目は属性の別名を出すだけの場合で、`CharField(source=...)`のほうが型情報も残ります。3つ目が一覧APIで、上の`obj.comments.count()`は1件ごとにクエリを発行します（次章で実測）。

## リレーションのネストと書き込み

関連のある投稿と著者・タグを返すとき、IDだけを返すか中身まで展開するかを選びます。

| 方法                     | 出力例                               | 書き込み                |
| ---------------------- | --------------------------------- | ------------------- |
| PrimaryKeyRelatedField | “author”: 1                       | そのまま可               |
| ネストしたSerializer        | “author”: {“id”: 1, “name”: “a0”} | create/updateの実装が必要 |

### 書き込み可能なネストと既定create()のAssertionError

著者をネストしたSerializerにPOSTのデータを渡すと、`is_valid()`は`True`を返すのに`save()`で止まりました。

```
class AuthorSerializer(serializers.ModelSerializer):
    class Meta:
        model = Author
        fields = ["id", "name"]

class PostSerializer(serializers.ModelSerializer):
    author = AuthorSerializer()

    class Meta:
        model = Post
        fields = ["id", "title", "author"]

AssertionError: The `.create()` method does not support writable nested fields by default.
Write an explicit `.create()` method for serializer `PostSerializer`, or set `read_only=True` on nested serializer fields.
```

エラー文が示すとおり、対処は2通りです。読み取りだけネストで返し、書き込みはIDで受ける設計にすると`create()`を書かずに済みます。

```
class PostSerializer(serializers.ModelSerializer):
    author = AuthorSerializer(read_only=True)
    author_id = serializers.PrimaryKeyRelatedField(
        source="author", queryset=Author.objects.all(), write_only=True
    )

    class Meta:
        model = Post
        fields = ["id", "title", "author", "author_id"]
```

関連先もまとめて作る必要があるなら、`author = AuthorSerializer()`（`read_only`なし）のSerializerに`create()`を書き、`validated_data`から入れ子の部分を取り出して先に保存します。2つの保存を同じトランザクションに入れ、投稿の保存に失敗したとき著者だけが残らないようにします。

```
from django.db import transaction

class PostWithAuthorSerializer(serializers.ModelSerializer):
    author = AuthorSerializer()

    class Meta:
        model = Post
        fields = ["id", "title", "author"]

    @transaction.atomic
    def create(self, validated_data):
        author_data = validated_data.pop("author")
        author = Author.objects.create(**author_data)
        return Post.objects.create(author=author, **validated_data)

# {'id': 2, 'title': 'n', 'author': {'id': 2, 'name': 'B'}}
```

既存の著者を使う場合は、前のコードの`author_id`で受けます。名前で`get_or_create()`すると、同名の別人を取り違えます。

入れ子の更新（`update()`）は、既存の関連を消すのか残すのかの仕様が画面ごとに違うため、DRFは既定の動きを持っていません。仕様を決めてから書きます。

## 一覧APIの性能：クエリ数と変換時間の実測

投稿100件（著者10人、各投稿にタグ3件・コメント3件）を、著者とタグをネストしたSerializerで出力したときのクエリ数を`CaptureQueriesContext`で数えました。

### ネストとSerializerMethodFieldのN+1

| クエリセット                                     | Serializer                       | クエリ数 |
| ------------------------------------------ | -------------------------------- | ---- |
| Post.objects.all()                         | 著者・タグをネスト                        | 201  |
| \+ select\_related(“author”)               | 同上                               | 101  |
| \+ prefetch\_related(“tags”)               | 同上                               | 2    |
| 同上                                         | \+ comments.count() のMethodField | 102  |
| \+ annotate(n\_comments=Count(“comments”)) | 件数はannotateの値を返す                 | 2    |

ForeignKeyは`select_related`、ManyToManyFieldと逆参照は`prefetch_related`で先に読みます。件数のような集計は、`SerializerMethodField`で数えずにクエリセットの`annotate()`で付けて、Serializerはその値を返すだけにします。

```
from django.db.models import Count
from rest_framework import viewsets

class PostViewSet(viewsets.ReadOnlyModelViewSet):
    serializer_class = PostSerializer

    def get_queryset(self):
        return (Post.objects
                .select_related("author")
                .prefetch_related("tags")
                .annotate(n_comments=Count("comments")))

class PostSerializer(serializers.ModelSerializer):
    author = AuthorSerializer(read_only=True)
    tags = TagSerializer(many=True, read_only=True)
    comment_count = serializers.IntegerField(source="n_comments", read_only=True)
    # ...
```

クエリセットの最適化はSerializerではなくビューの`get_queryset()`に置きます。Serializerはどんなクエリセットを渡されるかを知らないためです。N+1の検出方法や他のORMとの比較は[N+1問題とは？発行クエリ数の実測と検出方法・ORM別の対策](/tech/details/4262/)、`annotate`と集計の順序は[Django ORMのクエリ設計｜遅延評価と集計の順序・生SQLへ落とす判断](/tech/details/17082/)で詳しく扱っています。

### 10,000件の変換時間：ModelSerializerとvalues()の比較

クエリを1本に抑えても、Serializerは1件ごとにPythonのフィールド処理を通るため、件数に比例して時間がかかります。投稿10,000件を同じ5項目（id・title・price・author・created）のJSONにする処理を2通りで書き、レンダリング後のバイト列が完全に一致することを確かめてから、5回計測の中央値を取りました（Intel Core i9-9880H、SQLiteのメモリDB）。

| 処理（取得からJSONRendererまで）                  | 時間     |
| --------------------------------------- | ------ |
| only()で5列を取得→ModelSerializer(many=True) | 0.881秒 |
| values()で5列を取得→辞書を組み立て                  | 0.278秒 |

同じ出力でもSerializer経由は約3.2倍かかりました。画面向けの一覧APIは、ページネーションで1回の件数を絞ったうえで、応答時間を実データの件数と同時アクセス数で測って判断します。CSV出力や全件同期のように数万件を一度に返す処理で、出力項目・日時の書式・見せてよい列の制御をビュー側で同じに保てるなら、Serializerを通さず`values()`から直接組み立てる方を選びます。日時は`timezone.localtime()`で変換しないとUTCのまま出る点に注意します。

## よくある質問

### DjangoのSerializerの役割は何ですか？

モデルやクエリセットをJSONにしやすい辞書・リストへ変換することと、受け取ったデータ（パーサーが解析済みのもの）を検証して保存できる値にすることの2つです。JSON文字列との変換はレンダラーとパーサー、保存はModelSerializerの`create()`/`update()`（素のSerializerでは自分で書く）が担います。DRFでは検証の比重が大きく、型・必須・一意制約・独自ルールのチェックをSerializerにまとめます。

### シリアライザ（serializer）とはそもそも何ですか？

プログラム内のオブジェクトを、保存や通信に使えるバイト列や文字列へ変換する仕組みの総称です。PythonではJSONなら`json`モジュール、Pythonオブジェクトそのものなら`pickle`が該当します。`pickle`は信頼できないデータを読み込むと任意のコードが実行されうるため、外部入力には使いません（[安全でないデシリアライゼーションの解説](/tech/details/16369/)）。

### django.core.serializersとDRFのSerializerは何が違いますか？

Django本体の`django.core.serializers.serialize("json", queryset)`は、`[{"model": "app.post", "pk": 1, "fields": {...}}]`という固定の形で出力するもので、主にフィクスチャ（初期データ）の書き出しに使います。APIのレスポンスの形を決めたり、入力を検証したりする機能はありません。Web APIにはDRFのSerializerを使います。

### read\_onlyとwrite\_onlyの違いは何ですか？

`read_only=True`は出力にだけ含めて入力では無視し、`write_only=True`は入力でだけ受け取って出力に出しません。前者はidや作成日時、後者はパスワードに使います。

### DRFのSerializerとDjango NinjaのSchemaはどちらを使うべきですか？

既存のDRFプロジェクトや、ViewSet・認証・権限を一式そろえたい場合はDRFのSerializerです。型ヒント中心で書きたい、非同期のビューを主に使う、という新規プロジェクトならDjango Ninja（Pydanticベース）が候補になります。違いは[Django Ninjaとは？DRFとの違い・Schema・認証・非同期の実装を解説](/tech/details/3278/)で比べています。DRFでの外部連携APIの設計や構築を外部に任せたい場合は、[API開発・システム連携の支援](/service/system/api/)も行っています。

## 関連記事

- [Django REST Framework（DRF）とは？Djangoで最短にREST APIを作る仕組みと使いどころ](/tech/details/3277/)
- [N+1問題とは？発行クエリ数の実測と検出方法・ORM別の対策](/tech/details/4262/)
- [django-filterで一覧検索を実装する｜FilterSet設計とDRF指定の判断](/tech/details/17107/)
- [ReactとDjangoの連携方法｜DRF・CORS・JWT認証の実装手順と本番構成](/tech/details/4421/)
- [Djangoとは？特徴とMVTの仕組み・始め方をDjango 6.1で解説](/tech/details/3275/)

---

出典: [Django REST FrameworkのSerializer（シリアライザ）とは？使い方・検証・整形・ネストをDRF 3.18で実行して解説](<https://www.issoh.co.jp/tech/details/3276/>)（株式会社一創）
