Renderer classes, кастомные responses, форматирование вывода
DRF позволяет гибко настраивать формат и структуру ответов через renderer classes, кастомные Response и content negotiation.
Renderer преобразует данные ответа в нужный формат (JSON, HTML, XML и т.д.).
# settings.py
REST_FRAMEWORK = {
'DEFAULT_RENDERER_CLASSES': [
'rest_framework.renderers.JSONRenderer',
'rest_framework.renderers.BrowsableAPIRenderer',
]
}Классы:
JSONRenderer — JSON форматBrowsableAPIRenderer — HTML страница для браузера (включает формы)AdminRenderer — формат как в Django adminHTMLFormRenderer — для рендеринга формДля production часто отключают BrowsableAPIRenderer:
REST_FRAMEWORK = {
'DEFAULT_RENDERER_CLASSES': [
'rest_framework.renderers.JSONRenderer',
]
}Преимущества:
from rest_framework.renderers import JSONRenderer
class CustomJSONRenderer(JSONRenderer):
"""JSON с кастомной обёрткой ответа"""
def render(self, data, accepted_media_type=None, renderer_context=None):
# Обёртка всех ответов в единый формат
response = {
'success': True,
'data': data,
'meta': {
'timestamp': timezone.now().isoformat(),
'version': '1.0',
}
}
return super().render(response, accepted_media_type, renderer_context)Ответ:
{
"success": true,
"data": {...},
"meta": {
"timestamp": "2024-01-01T12:00:00Z",
"version": "1.0"
}
}DRF выбирает renderer на основе заголовка Accept:
# Запрос JSON
GET /api/articles/
Accept: application/json
# Запрос HTML (Browsable API)
GET /api/articles/
Accept: text/htmlfrom rest_framework.negotiation import BaseContentNegotiation
class IgnoreClientContentNegotiation(BaseContentNegotiation):
"""Игнорировать Accept заголовок, всегда использовать JSON"""
def select_parser(self, request, parsers):
return parsers[0]
def select_renderer(self, request, renderers, format_suffix=None):
from rest_framework.renderers import JSONRenderer
return (JSONRenderer(), 'application/json')
# settings.py
REST_FRAMEWORK = {
'DEFAULT_CONTENT_NEGOTIATION_CLASS':
'myapp.negotiation.IgnoreClientContentNegotiation',
}from rest_framework.response import Response
# Простой ответ
return Response(data)
# Со статусом
return Response(data, status=201)
# С заголовками
return Response(data, headers={'X-Custom': 'value'})from rest_framework.response import Response
from rest_framework.utils.encoders import JSONEncoder
import json
class APIResponse(Response):
"""Кастомный Response с единым форматом"""
def __init__(self, data=None, status=None, message='Success', **kwargs):
wrapped_data = {
'success': status is None or (200 <= status < 300),
'message': message,
'data': data,
}
super().__init__(data=wrapped_data, status=status, **kwargs)
# Использование
return APIResponse(article_data, message='Article created'){
"title": ["This field is required."],
"content": ["Ensure this field has no more than 1000 characters."]
}from rest_framework.views import exception_handler
def custom_exception_handler(exc, context):
response = exception_handler(exc, context)
if response is not None:
# Преобразуем формат ошибок
custom_response = {
'success': False,
'error': {
'code': response.status_code,
'message': response.data.get('detail', 'An error occurred'),
'fields': {}
}
}
# Собираем ошибки по полям
for key, value in response.data.items():
if key == 'detail':
continue
if isinstance(value, list):
custom_response['error']['fields'][key] = value
else:
custom_response['error']['message'] = value
response.data = custom_response
return response
# settings.py
REST_FRAMEWORK = {
'EXCEPTION_HANDLER': 'myapp.utils.custom_exception_handler',
}Ответ:
{
"success": false,
"error": {
"code": 400,
"message": "Validation failed",
"fields": {
"title": ["This field is required."],
"email": ["Enter a valid email address."]
}
}
}from rest_framework.response import Response
class ArticleViewSet(viewsets.ModelViewSet):
def retrieve(self, request, *args, **kwargs):
response = super().retrieve(request, *args, **kwargs)
response['X-Article-Views'] = self.get_object().views
response['Cache-Control'] = 'max-age=3600'
return responsefrom rest_framework.response import Response
def add_cors_headers(response):
response['Access-Control-Allow-Origin'] = '*'
response['Access-Control-Allow-Methods'] = 'GET, POST, PUT, DELETE'
response['Access-Control-Allow-Headers'] = 'Content-Type, Authorization'
return response
class MyView(APIView):
def get(self, request):
response = Response({'data': 'value'})
return add_cors_headers(response)from rest_framework.decorators import api_view
from django.utils.cache import patch_response_headers
from django.views.decorators.http import condition
from datetime import datetime
@api_view(['GET'])
@condition(last_modified_func=lambda r, pk: Article.objects.get(pk=pk).updated_at)
def article_detail(request, pk):
article = Article.objects.get(pk=pk)
serializer = ArticleSerializer(article)
response = Response(serializer.data)
patch_response_headers(response, cache_timeout=3600)
return responseЛогика:
If-Modified-Since с той же датой → возвращаем 304 Not Modifiedfrom rest_framework.response import Response
from django.utils.cache import patch_vary_headers
def article_detail(request, pk):
article = Article.objects.get(pk=pk)
etag = f'"{article.id}-{article.updated_at.timestamp()}"'
if request.META.get('HTTP_IF_NONE_MATCH') == etag:
return Response(status=304)
serializer = ArticleSerializer(article)
response = Response(serializer.data)
response['ETag'] = etag
return responsefrom rest_framework.pagination import PageNumberPagination
from rest_framework.response import Response
class CustomPagination(PageNumberPagination):
page_size = 20
def get_paginated_response(self, data):
return Response({
'success': True,
'meta': {
'total': self.page.paginator.count,
'page': self.page.number,
'per_page': self.page_size,
'total_pages': self.page.paginator.num_pages,
'has_next': self.page.has_next(),
'has_prev': self.page.has_previous(),
},
'data': data
})# settings.py
REST_FRAMEWORK = {
'URL_FORMAT_OVERRIDE': 'format',
}
# Запросы:
# GET /api/articles/ # JSON (по умолчанию)
# GET /api/articles/?format=json
# GET /api/articles/?format=html # Browsable API{success, data, error, meta}Вопросы ещё не добавлены
Вопросы для этой подтемы ещё не добавлены.