Content negotiation en DRF: JSON, XML, CSV y YAML

Avanzado
Django
Django
Actualizado: 19/04/2026

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 BrowsableAPIRenderer para no exponer una pagina HTML que duplica funcionalidades de Swagger. Lo dejas activo solo en DEBUG=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-csv y 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 - Autor del tutorial

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