Que es content negotiation
El cliente HTTP indica que formatos acepta y con que preferencia mediante la cabecera Accept.
GET /api/productos/ HTTP/1.1
Accept: application/json, application/xml;q=0.5
Esto dice "preferiria JSON, pero acepto XML si no hay otra cosa". El servidor mira los formatos disponibles, elige el de mayor q y devuelve el Content-Type adecuado.
DRF implementa esto con dos piezas:
- Renderers: convierten objetos Python en bytes de respuesta (JSON, XML, CSV, ...).
- Parsers: convierten bytes de entrada (
request.body) en objetos Python.
Por defecto DRF instala JSONRenderer y BrowsableAPIRenderer (la pagina HTML interactiva en navegador).
Configuracion global
# settings.py
REST_FRAMEWORK = {
"DEFAULT_RENDERER_CLASSES": [
"rest_framework.renderers.JSONRenderer",
"rest_framework.renderers.BrowsableAPIRenderer",
"rest_framework_xml.renderers.XMLRenderer",
"rest_framework_csv.renderers.CSVRenderer",
"rest_framework_yaml.renderers.YAMLRenderer",
],
"DEFAULT_PARSER_CLASSES": [
"rest_framework.parsers.JSONParser",
"rest_framework_xml.parsers.XMLParser",
"rest_framework_yaml.parsers.YAMLParser",
"rest_framework.parsers.MultiPartParser",
"rest_framework.parsers.FormParser",
],
}
Instalacion previa:
pip install djangorestframework-xml djangorestframework-csv djangorestframework-yaml
En produccion suele convenir desactivar
BrowsableAPIRendererpara no exponer una pagina HTML que duplica funcionalidades de Swagger. Lo dejas activo solo enDEBUG=True.
Configuracion por vista
from rest_framework.viewsets import ModelViewSet
from rest_framework.renderers import JSONRenderer
from rest_framework_csv.renderers import CSVRenderer
class ProductoViewSet(ModelViewSet):
queryset = Producto.objects.all()
serializer_class = ProductoSerializer
renderer_classes = [JSONRenderer, CSVRenderer]
Asi este endpoint solo responde JSON o CSV, ignorando los demas globales.
Probar la negociacion
curl -H "Accept: application/json" http://localhost:8000/api/productos/
# -> JSON
curl -H "Accept: application/xml" http://localhost:8000/api/productos/
# -> XML
curl -H "Accept: text/csv" http://localhost:8000/api/productos/
# -> CSV (con cabecera y filas)
curl -H "Accept: application/yaml" http://localhost:8000/api/productos/
# -> YAML
Forzar formato con sufijo URL
Algunos clientes (sobre todo navegadores) no envian Accept correctamente. DRF soporta un sufijo en la URL.
# urls.py
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from rest_framework.urlpatterns import format_suffix_patterns
from productos.views import ProductoViewSet
router = DefaultRouter()
router.register(r"productos", ProductoViewSet, basename="productos")
urlpatterns = format_suffix_patterns(router.urls, allowed=["json", "xml", "csv", "yaml"])
Ahora GET /api/productos/.csv devuelve CSV sin necesidad del header.
Renderer custom: Excel xlsx
Para casos donde necesitas un formato no estandar, escribe un renderer.
# productos/renderers.py
import io
import openpyxl
from rest_framework.renderers import BaseRenderer
class XLSXRenderer(BaseRenderer):
media_type = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
format = "xlsx"
charset = None
render_style = "binary"
def render(self, data, media_type=None, renderer_context=None):
wb = openpyxl.Workbook()
ws = wb.active
if isinstance(data, dict) and "results" in data:
data = data["results"]
if not data:
return b""
headers = list(data[0].keys())
ws.append(headers)
for row in data:
ws.append([row.get(h) for h in headers])
buf = io.BytesIO()
wb.save(buf)
return buf.getvalue()
class ProductoViewSet(ModelViewSet):
renderer_classes = [JSONRenderer, XLSXRenderer]
curl -H "Accept: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" \
http://localhost:8000/api/productos/ -o productos.xlsx
Para CSV recomiendo
djangorestframework-csvy para Excel hacer un endpoint dedicado (/api/productos/exportar/) en vez de mezclar. Asi ningun cliente accidentalmente recibe binario inesperado.
Parser custom
Mismo principio para la entrada. Si tu integrador envia CSV con productos:
import csv
from io import StringIO
from rest_framework.parsers import BaseParser
class CSVProductoParser(BaseParser):
media_type = "text/csv"
def parse(self, stream, media_type=None, parser_context=None):
text = stream.read().decode("utf-8")
reader = csv.DictReader(StringIO(text))
return list(reader)
class ProductoViewSet(ModelViewSet):
parser_classes = [JSONParser, CSVProductoParser]
def create(self, request, *args, **kwargs):
# request.data ya esta parseado por JSON o CSV segun Content-Type
if isinstance(request.data, list):
...
Internacionalizacion en respuestas
DRF respeta Accept-Language si configuras LocaleMiddleware. Util para mensajes de error traducidos.
MIDDLEWARE = [
"django.middleware.locale.LocaleMiddleware",
...
]
LANGUAGES = [("es", "Espanol"), ("en", "English")]
LOCALE_PATHS = [BASE_DIR / "locale"]
from django.utils.translation import gettext_lazy as _
class ProductoSerializer(serializers.ModelSerializer):
nombre = serializers.CharField(label=_("Nombre"), help_text=_("Nombre comercial"))
Accept-Language: en-> errores y labels en ingles.es-> espanol.
Diagrama del proceso
flowchart LR
C[Cliente] -->|Accept: text/csv| API[DRF]
API --> NEG{Renderer disponible?}
NEG -->|si| RENDER[CSVRenderer.render]
NEG -->|no| ERR[406 Not Acceptable]
RENDER --> RES[Response Content-Type: text/csv]
RES --> C
Documentar formatos en OpenAPI
drf-spectacular detecta los renderers configurados y los anade automaticamente a produces en cada operacion del schema. Si tienes JSON + CSV, el schema declarara ambos Content-Type en las respuestas. Esto permite que herramientas como Postman o Insomnia muestren al integrador todas las opciones.
Buenas practicas
- JSON sigue siendo el formato por defecto. Anade XML/CSV/YAML solo si tienes integradores que los pidan.
- Endpoints de export dedicados (
/api/productos/exportar/?formato=xlsx) son mas claros que content negotiation universal cuando el formato altera la semantica (paginar CSV no tiene sentido). - Cuidado con el tamano: CSV de 100k filas en una respuesta sincrona timeoutea. Para exports grandes, encola y devuelve un link de descarga (S3 + presigned URL).
- Versiona los renderers si cambian: una integracion B2B con XML legacy puede romperse al cambiar la estructura.
Alan Sastre
Ingeniero de Software y formador, CEO en CertiDevs
Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, Django es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.
Más tutoriales de Django
Explora más contenido relacionado con Django y continúa aprendiendo con nuestros tutoriales gratuitos.
Aprendizajes de esta lección
Comprender HTTP content negotiation con Accept header. Configurar DEFAULT_RENDERER_CLASSES y DEFAULT_PARSER_CLASSES. Anadir renderers para XML, CSV y YAML. Crear un renderer custom (ej. PDF, Excel). Forzar formato con sufijo URL (.json, .csv) usando format_suffix_patterns. Documentar formatos en OpenAPI.
Cursos que incluyen esta lección
Esta lección forma parte de los siguientes cursos estructurados con rutas de aprendizaje