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を作る仕組みと使いどころで扱い、本記事は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(2026年10月6日公開)で、Python 3.10以上、Django 5.2・6.0・6.1に対応します。リリースノートによると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に変わったためです。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で弾くかの設計は入力バリデーションとは?防げる攻撃の範囲と許可リスト設計を実装目線で解説で扱っています。
一意制約に対する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番目」でエラーを拾っている場合は、更新後に表示が崩れます。公式ドキュメントは、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認証の実装手順と本番構成で扱っています。
ログイン入力に対する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別の対策、annotateと集計の順序はDjango ORMのクエリ設計|遅延評価と集計の順序・生SQLへ落とす判断で詳しく扱っています。
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は信頼できないデータを読み込むと任意のコードが実行されうるため、外部入力には使いません(安全でないデシリアライゼーションの解説)。
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・認証・非同期の実装を解説で比べています。DRFでの外部連携APIの設計や構築を外部に任せたい場合は、API開発・システム連携の支援も行っています。