タイトル: モデルのjson化とレスポンス
SEOタイトル: Django モデルの JSON 化とレスポンス完全ガイド(JsonResponse / serializers / DRF)
| この記事の要点 |
|
4 つのアプローチ
| 方法 | 用途 | 長所 | 短所 |
|---|---|---|---|
1. JsonResponse 直接 | 小規模 API、単純レコード | 追加依存ゼロ、最速 | フィールド指定が手動 |
2. django.core.serializers | QuerySet 丸ごと | 標準同梱 | 独特なラップ形式 |
3. DRF ModelSerializer | ★ 本格 API | 宣言的、バリデーション付き | パッケージ追加 |
4. 自作 to_dict() | カスタム整形 | 柔軟 | メンテ負担 |
方法1: JsonResponse で直接返す
from django.http import JsonResponse
from .models import Book
def book_detail(request, pk):
book = Book.objects.get(pk=pk)
return JsonResponse({
'id': book.id,
'title': book.title,
'isbn': book.isbn,
'published_at': book.published_at.isoformat(), # date → ISO 文字列
'author': {
'id': book.author.id,
'name': book.author.name,
},
})
# QuerySet をリストで返す
def book_list(request):
qs = Book.objects.select_related('author').all()
data = [{
'id': b.id,
'title': b.title,
'author': b.author.name,
} for b in qs]
return JsonResponse({'count': len(data), 'results': data})
# ↑ safe=False が必要な場合は: JsonResponse(data, safe=False)
safe=False の話
JsonResponse はデフォルトで辞書のみ受け付けます。リストを直接返したい場合は safe=False を付けます:
return JsonResponse([{'a': 1}, {'b': 2}], safe=False)
# safe=True (既定) で list を渡すと TypeError 発生
# JSON Hijacking 対策で禁止されている、ラップ推奨:
return JsonResponse({'data': [{'a': 1}, {'b': 2}]})
方法2: django.core.serializers
from django.core import serializers
from django.http import HttpResponse
from .models import Book
def book_list_json(request):
qs = Book.objects.all()
data = serializers.serialize('json', qs, fields=('title', 'isbn'))
return HttpResponse(data, content_type='application/json')
# 出力例
# [
# {"model": "library.book", "pk": 1,
# "fields": {"title": "Python入門", "isbn": "9784..."}},
# ...
# ]
独自のフォーマット(model / pk / fields)でラップされるため、フロント側が普通の JSON を期待していると扱いにくい点に注意。fixture 形式に近い。
方法3: Django REST Framework(推奨)
pip install djangorestframework
# settings.py
INSTALLED_APPS += ['rest_framework']
ModelSerializer
# serializers.py
from rest_framework import serializers
from .models import Book, Author
class AuthorSerializer(serializers.ModelSerializer):
class Meta:
model = Author
fields = ['id', 'name', 'birth_date']
class BookSerializer(serializers.ModelSerializer):
author = AuthorSerializer(read_only=True) # ネスト表示
author_id = serializers.PrimaryKeyRelatedField(
queryset=Author.objects.all(), source='author', write_only=True
)
# 計算項目
is_recent = serializers.SerializerMethodField()
class Meta:
model = Book
fields = ['id', 'title', 'isbn', 'published_at',
'author', 'author_id', 'is_recent']
read_only_fields = ['id']
def get_is_recent(self, obj):
from datetime import date
return (date.today() - obj.published_at).days < 90
# views.py
from rest_framework.decorators import api_view
from rest_framework.response import Response
from .serializers import BookSerializer
@api_view(['GET'])
def book_list(request):
qs = Book.objects.select_related('author').all()
ser = BookSerializer(qs, many=True)
return Response(ser.data)
@api_view(['POST'])
def book_create(request):
ser = BookSerializer(data=request.data)
ser.is_valid(raise_exception=True)
ser.save()
return Response(ser.data, status=201)
ViewSet + Router でフル CRUD
from rest_framework import viewsets
from rest_framework.routers import DefaultRouter
class BookViewSet(viewsets.ModelViewSet):
queryset = Book.objects.select_related('author')
serializer_class = BookSerializer
router = DefaultRouter()
router.register(r'books', BookViewSet)
urlpatterns = router.urls
# → /books/ (GET, POST), /books/<id>/ (GET, PUT, PATCH, DELETE) 自動生成
Date / UUID / Decimal の扱い
Python 標準の json.dumps() は date / datetime / Decimal / UUID をそのままシリアライズできません:
import json
from datetime import date
from decimal import Decimal
from uuid import uuid4
data = {'d': date.today(), 'p': Decimal('123.45'), 'u': uuid4()}
# ❌ TypeError: Object of type date is not JSON serializable
json.dumps(data)
# ✅ Django 標準のエンコーダを使う
from django.core.serializers.json import DjangoJSONEncoder
json.dumps(data, cls=DjangoJSONEncoder)
# {"d": "2025-01-15", "p": "123.45", "u": "..."}
# JsonResponse は内部で DjangoJSONEncoder を使うので OK
from django.http import JsonResponse
JsonResponse(data) # 動く
各型の出力フォーマット
| Python 型 | JSON 出力 |
|---|---|
date | "2025-01-15" |
datetime | "2025-01-15T10:00:00+09:00" |
time | "10:00:00" |
timedelta | "3 12:00:00" |
Decimal | "123.45"(文字列) |
UUID | "550e8400-e29b-41d4-a716-446655440000" |
Promise (lazy) | str() 評価後の文字列 |
ネスト関係の扱い
# Author → Book[]
class AuthorWithBooksSerializer(serializers.ModelSerializer):
books = BookSerializer(many=True, read_only=True) # related_name='books' で逆参照
class Meta:
model = Author
fields = ['id', 'name', 'books']
# 出力
# {
# "id": 1, "name": "村上春樹",
# "books": [
# {"id": 11, "title": "...", "isbn": "..."},
# {"id": 12, "title": "...", "isbn": "..."}
# ]
# }
# depth で簡易ネスト(書き込みは不可)
class BookSerializer(serializers.ModelSerializer):
class Meta:
model = Book
fields = '__all__'
depth = 1 # ForeignKey が自動ネスト
SerializerMethodField で計算項目
class BookSerializer(serializers.ModelSerializer):
days_since_published = serializers.SerializerMethodField()
author_name = serializers.CharField(source='author.name', read_only=True)
full_url = serializers.SerializerMethodField()
class Meta:
model = Book
fields = ['id', 'title', 'author_name',
'days_since_published', 'full_url']
def get_days_since_published(self, obj):
from datetime import date
return (date.today() - obj.published_at).days
def get_full_url(self, obj):
request = self.context.get('request')
return request.build_absolute_uri(f'/books/{obj.id}/') if request else None
パフォーマンス: N+1 を避ける
# ❌ N+1: author を毎回 SELECT
qs = Book.objects.all()
BookSerializer(qs, many=True).data
# SELECT * FROM book;
# SELECT * FROM author WHERE id=1; ← N 回
# SELECT * FROM author WHERE id=2;
# ...
# ✅ select_related (1:1, ForeignKey)
qs = Book.objects.select_related('author')
# ✅ prefetch_related (1:N, M:N)
qs = Author.objects.prefetch_related('books')
# ✅ さらに細かく
qs = Author.objects.prefetch_related(
Prefetch('books', queryset=Book.objects.filter(is_published=True))
)
ページネーション
# settings.py
REST_FRAMEWORK = {
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE': 20,
}
# 出力
# {
# "count": 145,
# "next": "http://localhost:8000/books/?page=2",
# "previous": null,
# "results": [ ... ]
# }
# Cursor 方式(大量データ向け)
class BookCursor(CursorPagination):
ordering = '-published_at'
page_size = 50
カスタム to_dict() アプローチ
# models.py に直接生やす
class Book(models.Model):
title = models.CharField(max_length=200)
isbn = models.CharField(max_length=13)
def to_dict(self, *, with_author=False):
d = {
'id': self.id,
'title': self.title,
'isbn': self.isbn,
}
if with_author:
d['author'] = self.author.to_dict()
return d
# views.py
def book_list(request):
qs = Book.objects.select_related('author')
return JsonResponse({'results': [b.to_dict(with_author=True) for b in qs]})
FAQ
Q: JsonResponse と HttpResponse の違いは?
A: JsonResponse は内部で json.dumps(cls=DjangoJSONEncoder) を呼び、Content-Type: application/json を自動付与します。素の HttpResponse だと両方手動。
Q: 日付のタイムゾーンが UTC で返ってしまう
A: settings.py の TIME_ZONE と USE_TZ=True を確認。表示用に Asia/Tokyo へ変換するには django.utils.timezone.localtime()。
Q: DRF を入れずに JWT 認証だけ追加したい
A: PyJWT で自前実装が可能ですが、djangorestframework-simplejwt 経由が圧倒的に安全・速いです。