Django Rest Framework: devuelve dinámicamente un subconjunto de campos

100

Problema

Como se recomienda en la publicación de blog Mejores prácticas para diseñar una API RESTful pragmática , me gustaría agregar un fieldsparámetro de consulta a una API basada en Django Rest Framework que permite al usuario seleccionar solo un subconjunto de campos por recurso.

Ejemplo

Serializador:

class IdentitySerializer(serializers.HyperlinkedModelSerializer):
    class Meta:
        model = models.Identity
        fields = ('id', 'url', 'type', 'data')

Una consulta regular devolvería todos los campos.

GET /identities/

[
  {
    "id": 1,
    "url": "http://localhost:8000/api/identities/1/",
    "type": 5,
    "data": "John Doe"
  },
  ...
]

Una consulta con el fieldsparámetro solo debe devolver un subconjunto de los campos:

GET /identities/?fields=id,data

[
  {
    "id": 1,
    "data": "John Doe"
  },
  ...
]

Una consulta con campos no válidos debe ignorar los campos no válidos o generar un error de cliente.

Objetivo

¿Es esto posible fuera de la caja de alguna manera? Si no es así, ¿cuál es la forma más sencilla de implementar esto? ¿Existe un paquete de terceros que ya haga esto?

Danilo Bargen
fuente

Respuestas:

121

Puede anular el __init__método del serializador y establecer el fieldsatributo dinámicamente, según los parámetros de consulta. Puede acceder al requestobjeto en todo el contexto, pasado al serializador.

Aquí hay una copia y pegado del ejemplo de documentación de Django Rest Framework sobre el tema:

from rest_framework import serializers

class DynamicFieldsModelSerializer(serializers.ModelSerializer):
    """
    A ModelSerializer that takes an additional `fields` argument that
    controls which fields should be displayed.
    """

    def __init__(self, *args, **kwargs):
        # Instantiate the superclass normally
        super(DynamicFieldsModelSerializer, self).__init__(*args, **kwargs)

        fields = self.context['request'].query_params.get('fields')
        if fields:
            fields = fields.split(',')
            # Drop any fields that are not specified in the `fields` argument.
            allowed = set(fields)
            existing = set(self.fields.keys())
            for field_name in existing - allowed:
                self.fields.pop(field_name)


class UserSerializer(DynamicFieldsModelSerializer, serializers.HyperlinkedModelSerializer):

    class Meta:
        model = User
        fields = ('url', 'username', 'email')
YAtOff
fuente
4
Finalmente llegué a implementar esto, ¡y funciona perfectamente! Gracias. Terminé escribiendo un mixin para esto, la composición es un poco más flexible que la subclasificación :) gist.github.com/dbrgn/4e6fc1fe5922598592d6
Danilo Bargen
8
Tendrá que cambiar QUERY_PARAMSa query_paramsen versiones recientes de Django, pero aparte de eso, esto funciona a las mil maravillas.
Myk Willis
3
Probablemente debería comprobar que requestsexiste como miembro de context. Si bien lo hace en producción, no lo hace cuando se ejecutan pruebas unitarias que crean los objetos manualmente.
smitec
21
Para su información: este ejemplo es una copia literal de la documentación de DRF que se encuentra aquí: django-rest-framework.org/api-guide/serializers/#example Es una mala forma no proporcionar un enlace a los autores originales
Alex Bausk
3
La documentación DRF , de la cual se copió esta respuesta, se ha mejorado desde que se publicó esta respuesta.
Chris
51

Esta funcionalidad está disponible en un paquete de terceros .

pip install djangorestframework-queryfields

Declare su serializador así:

from rest_framework.serializers import ModelSerializer
from drf_queryfields import QueryFieldsMixin

class MyModelSerializer(QueryFieldsMixin, ModelSerializer):
    ...

Entonces, los campos ahora se pueden especificar (del lado del cliente) mediante el uso de argumentos de consulta:

GET /identities/?fields=id,data

El filtrado de exclusión también es posible, por ejemplo, para devolver todos los campos excepto id:

GET /identities/?fields!=id

descargo de responsabilidad: soy el autor / mantenedor.

wim
fuente
1
Hola. ¿Cuál es la diferencia entre esto y github.com/dbrgn/drf-dynamic-fields (como se vincula en los comentarios de la respuesta elegida)?
Danilo Bargen
5
Gracias, eché un vistazo a esa implementación y parece que es la misma idea básica. Pero la dbrgnimplementación tiene algunas diferencias: 1. no admite excluir con fields!=key1,key2. 2. también modifica los serializadores fuera del contexto de la solicitud GET, lo que puede romper y romperá algunas solicitudes PUT / POST. 3. no acumula campos con fields=key1&fields=key2, por ejemplo , lo que es bueno para las aplicaciones ajax. También tiene cobertura de prueba cero, lo cual es algo inusual en OSS.
wim
1
@wim ¿Qué versiones de DRF y Django admite su biblioteca? No encontré nada en los documentos.
pawelswiecki
1
Django 1.7-1.11 +, básicamente cualquier configuración que admita DRF. Este comentario puede quedar desactualizado, así que consulte la matriz de prueba del IC aquí .
wim
1
Funciona muy bien para mí: Django == 2.2.7, djangorestframework == 3.10.3, djangorestframework-queryfields == 1.0.0
Neeraj Kashyap
7

serializers.py

class DynamicFieldsSerializerMixin(object):

    def __init__(self, *args, **kwargs):
        # Don't pass the 'fields' arg up to the superclass
        fields = kwargs.pop('fields', None)

        # Instantiate the superclass normally
        super(DynamicFieldsSerializerMixin, self).__init__(*args, **kwargs)

        if fields is not None:
            # Drop any fields that are not specified in the `fields` argument.
            allowed = set(fields)
            existing = set(self.fields.keys())
            for field_name in existing - allowed:
                self.fields.pop(field_name)


class UserSerializer(DynamicFieldsSerializerMixin, serializers.HyperlinkedModelSerializer):

    password = serializers.CharField(
        style={'input_type': 'password'}, write_only=True
    )

    class Meta:
        model = User
        fields = ('id', 'username', 'password', 'email', 'first_name', 'last_name')


    def create(self, validated_data):
        user = User.objects.create(
            username=validated_data['username'],
            email=validated_data['email'],
            first_name=validated_data['first_name'],
            last_name=validated_data['last_name']
        )

        user.set_password(validated_data['password'])
        user.save()

        return user

views.py

class DynamicFieldsViewMixin(object):

 def get_serializer(self, *args, **kwargs):

    serializer_class = self.get_serializer_class()

    fields = None
    if self.request.method == 'GET':
        query_fields = self.request.QUERY_PARAMS.get("fields", None)

        if query_fields:
            fields = tuple(query_fields.split(','))


    kwargs['context'] = self.get_serializer_context()
    kwargs['fields'] = fields

    return serializer_class(*args, **kwargs)



class UserList(DynamicFieldsViewMixin, ListCreateAPIView):
    queryset = User.objects.all()
    serializer_class = UserSerializer
Austin Malerba
fuente
3

Configurar una nueva clase de serializador de paginación

from rest_framework import pagination, serializers

class DynamicFieldsPaginationSerializer(pagination.BasePaginationSerializer):
    """
    A dynamic fields implementation of a pagination serializer.
    """
    count = serializers.Field(source='paginator.count')
    next = pagination.NextPageField(source='*')
    previous = pagination.PreviousPageField(source='*')

    def __init__(self, *args, **kwargs):
        """
        Override init to add in the object serializer field on-the-fly.
        """
        fields = kwargs.pop('fields', None)
        super(pagination.BasePaginationSerializer, self).__init__(*args, **kwargs)
        results_field = self.results_field
        object_serializer = self.opts.object_serializer_class

        if 'context' in kwargs:
            context_kwarg = {'context': kwargs['context']}
        else:
            context_kwarg = {}

        if fields:
            context_kwarg.update({'fields': fields})

        self.fields[results_field] = object_serializer(source='object_list',
                                                       many=True,
                                                       **context_kwarg)


# Set the pagination serializer setting
REST_FRAMEWORK = {
    # [...]
    'DEFAULT_PAGINATION_SERIALIZER_CLASS': 'DynamicFieldsPaginationSerializer',
}

Hacer serializador dinámico

from rest_framework import serializers

class DynamicFieldsModelSerializer(serializers.ModelSerializer):
    """
    A ModelSerializer that takes an additional `fields` argument that
    controls which fields should be displayed.

    See:
        http://tomchristie.github.io/rest-framework-2-docs/api-guide/serializers
    """

    def __init__(self, *args, **kwargs):
        # Don't pass the 'fields' arg up to the superclass
        fields = kwargs.pop('fields', None)

        # Instantiate the superclass normally
        super(DynamicFieldsModelSerializer, self).__init__(*args, **kwargs)

        if fields:
            # Drop any fields that are not specified in the `fields` argument.
            allowed = set(fields)
            existing = set(self.fields.keys())
            for field_name in existing - allowed:
                self.fields.pop(field_name)
# Use it
class MyPonySerializer(DynamicFieldsModelSerializer):
    # [...]

Por último, use una combinación de homemage para sus APIViews

class DynamicFields(object):
    """A mixins that allows the query builder to display certain fields"""

    def get_fields_to_display(self):
        fields = self.request.GET.get('fields', None)
        return fields.split(',') if fields else None

    def get_serializer(self, instance=None, data=None, files=None, many=False,
                       partial=False, allow_add_remove=False):
        """
        Return the serializer instance that should be used for validating and
        deserializing input, and for serializing output.
        """
        serializer_class = self.get_serializer_class()
        context = self.get_serializer_context()
        fields = self.get_fields_to_display()
        return serializer_class(instance, data=data, files=files,
                                many=many, partial=partial,
                                allow_add_remove=allow_add_remove,
                                context=context, fields=fields)

    def get_pagination_serializer(self, page):
        """
        Return a serializer instance to use with paginated data.
        """
        class SerializerClass(self.pagination_serializer_class):
            class Meta:
                object_serializer_class = self.get_serializer_class()

        pagination_serializer_class = SerializerClass
        context = self.get_serializer_context()
        fields = self.get_fields_to_display()
        return pagination_serializer_class(instance=page, context=context, fields=fields)

class MyPonyList(DynamicFields, generics.ListAPIView):
    # [...]

Solicitud

Ahora, cuando solicita un recurso, puede agregar un parámetro fieldspara mostrar solo los campos especificados en la URL. /?fields=field1,field2

Puede encontrar un recordatorio aquí: https://gist.github.com/Kmaschta/e28cf21fb3f0b90c597a

Kmaschta
fuente
2

Puede probar Dynamic REST , que tiene soporte para campos dinámicos (inclusión, exclusión), objetos incrustados / descargados, filtrado, ordenamiento, paginación y más.

blueFast
fuente
1

Para datos anidados, estoy usando Django Rest Framework con el paquete recomendado en los documentos , drf-flexfields

Esto le permite restringir los campos devueltos tanto en el objeto principal como en el secundario. Las instrucciones en el archivo Léame son buenas, solo algunas cosas a tener en cuenta:

La URL parece necesitar el / como este '/ person /? Expand = country & fields = id, name, country' en lugar de como está escrito en el archivo Léame '/ person? Expand = country & fields = id, name, country'

El nombre del objeto anidado y su nombre relacionado deben ser completamente coherentes, lo que no es necesario de otro modo.

Si tiene 'muchos', por ejemplo, un país puede tener muchos estados, deberá establecer 'muchos': Verdadero en el serializador como se describe en los documentos.

Pequeño cerebro
fuente
1

Si desea algo flexible como GraphQL, puede usar django-restql . Admite datos anidados (tanto planos como iterables).

Ejemplo

from rest_framework import serializers
from django.contrib.auth.models import User
from django_restql.mixins import DynamicFieldsMixin

class UserSerializer(DynamicFieldsMixin, serializers.ModelSerializer):
    class Meta:
        model = User
        fields = ('id', 'username', 'email', 'groups')

Una solicitud regular devuelve todos los campos.

GET /users

    [
      {
        "id": 1,
        "username": "yezyilomo",
        "email": "[email protected]",
        "groups": [1,2]
      },
      ...
    ]

Por queryotro lado, una solicitud con el parámetro devuelve solo un subconjunto de los campos:

GET /users/?query={id, username}

    [
      {
        "id": 1,
        "username": "yezyilomo"
      },
      ...
    ]

Con django-restql puede acceder a campos anidados de cualquier nivel. P.ej

GET /users/?query={id, username, date_joined{year}}

    [
      {
        "id": 1,
        "username": "yezyilomo",
        "date_joined": {
            "year": 2018
        }
      },
      ...
    ]

Para campos anidados iterables, por ejemplo, grupos de usuarios.

GET /users/?query={id, username, groups{id, name}}

    [
      {
        "id": 1,
        "username": "yezyilomo",
        "groups": [
            {
                "id": 2,
                "name": "Auth_User"
            }
        ]
      },
      ...
    ]
Yezy Ilomo
fuente