Django Rest Framework: retorna dinamicamente o subconjunto de campos


100

Problema

Conforme recomendado na postagem do blog Best Practices para projetar uma API RESTful pragmática , gostaria de adicionar um fieldsparâmetro de consulta a uma API baseada no Django Rest Framework que permite ao usuário selecionar apenas um subconjunto de campos por recurso.

Exemplo

Serializer:

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

Uma consulta regular retornaria todos os campos.

GET /identities/

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

Uma consulta com o fieldsparâmetro deve retornar apenas um subconjunto dos campos:

GET /identities/?fields=id,data

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

Uma consulta com campos inválidos deve ignorar os campos inválidos ou gerar um erro do cliente.

Objetivo

Isso é possível fora da caixa de alguma forma? Se não, qual é a maneira mais simples de implementar isso? Existe um pacote de terceiros que já faz isso?

Respostas:


121

Você pode substituir o __init__método do serializador e definir o fieldsatributo dinamicamente, com base nos parâmetros de consulta. Você pode acessar o requestobjeto em todo o contexto, passado para o serializador.

Aqui está um exemplo de documentação de copiar e colar do Django Rest Framework sobre o assunto:

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')

4
Finalmente resolvi implementar isso e funciona perfeitamente! Obrigado. Acabei escrevendo um mixin para isso, a composição é um pouco mais flexível do que a subclasse :) gist.github.com/dbrgn/4e6fc1fe5922598592d6
— Danilo Bargen

8
Você precisará mudar QUERY_PARAMSpara query_paramsnas versões recentes do Django, mas fora isso, funciona perfeitamente.
— Myk Willis

3
Você provavelmente deve verificar se requestsexiste como membro de context. Enquanto faz na produção, não faz isso ao executar testes de unidade que criam os objetos manualmente.
— smitec

21
Para sua informação: Este exemplo é uma cópia literal da documentação DRF encontrada aqui: django-rest-framework.org/api-guide/serializers/#example É uma má forma de não fornecer link para os autores originais
— Alex Bausk

3
A documentação DRF , da qual esta resposta foi copiada, foi aprimorada desde que esta resposta foi postada.
— Chris

51

Esta funcionalidade está disponível em um pacote de terceiros .

pip install djangorestframework-queryfields

Declare seu serializador assim:

from rest_framework.serializers import ModelSerializer
from drf_queryfields import QueryFieldsMixin

class MyModelSerializer(QueryFieldsMixin, ModelSerializer):
    ...

Em seguida, os campos agora podem ser especificados (lado do cliente) usando argumentos de consulta:

GET /identities/?fields=id,data

A filtragem de exclusão também é possível, por exemplo, para retornar todos os campos, exceto id:

GET /identities/?fields!=id

isenção de responsabilidade: eu sou o autor / mantenedor.


1
Oi. Qual é a diferença entre this e github.com/dbrgn/drf-dynamic-fields (conforme link nos comentários da resposta escolhida)?
— Danilo Bargen

5
Obrigado, dei uma olhada nessa implementação e parece que é a mesma ideia básica. Mas a dbrgnimplementação tem algumas diferenças: 1. não suporta excluir com fields!=key1,key2. 2. também modifica serializadores fora do contexto de solicitação GET, o que pode e irá interromper algumas solicitações PUT / POST. 3. não acumula campos com fields=key1&fields=key2, por exemplo , o que é útil para aplicativos ajax. Ele também tem cobertura de teste zero, o que é um tanto incomum no OSS.
— wim

1
@wim Quais versões de DRF e Django sua biblioteca suporta? Não encontrei nada nos documentos.
— pawelswiecki

1
Django 1.7-1.11 +, basicamente qualquer configuração que o DRF suporta. Este comentário pode estar desatualizado, portanto, verifique a matriz de teste do IC aqui .
— wim

1
Funciona muito bem para mim: 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

3

Configure uma nova classe de serializador de paginação

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',
}

Faça 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 um mixin de imagens caseiras para suas 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):
    # [...]

Solicitação

Agora, ao solicitar um recurso, você pode adicionar um parâmetro fieldspara mostrar apenas os campos especificados no url. /?fields=field1,field2

Você pode encontrar um lembrete aqui: https://gist.github.com/Kmaschta/e28cf21fb3f0b90c597a


2

Você pode tentar REST dinâmico , que tem suporte para campos dinâmicos (inclusão, exclusão), objetos incorporados / sideload, filtragem, ordenação, paginação e muito mais.



1

Para dados aninhados, estou usando Django Rest Framework com o pacote recomendado na documentação , drf-flexfields

Isso permite que você restrinja os campos retornados nos objetos pai e filho. As instruções no leia-me são boas, apenas algumas coisas a serem observadas:

O URL parece precisar do / assim '/ pessoa /? Expand = país & campos = id, nome, país' em vez de como escrito no readme '/ pessoa? Expand = país & campos = id, nome, país'

A nomenclatura do objeto aninhado e seu nome relacionado precisam ser completamente consistentes, o que não é necessário de outra forma.

Se você tiver 'muitos', por exemplo, um país pode ter muitos estados, você precisará definir 'muitos': True no serializador conforme descrito nos documentos.


1

Se você quiser algo flexível como GraphQL, você pode usar django-restql . Ele oferece suporte a dados aninhados (simples e iteráveis).

Exemplo

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')

Uma solicitação regular retorna todos os campos.

GET /users

    [
      {
        "id": 1,
        "username": "yezyilomo",
        "email": "yezileliilomo@hotmail.com",
        "groups": [1,2]
      },
      ...
    ]

Uma solicitação com o queryparâmetro, por outro lado, retorna apenas um subconjunto dos campos:

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

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

Com django-restql você pode acessar campos aninhados de qualquer nível. Por exemplo

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

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

Para campos aninhados iteráveis, por exemplo, grupos de usuários.

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

    [
      {
        "id": 1,
        "username": "yezyilomo",
        "groups": [
            {
                "id": 2,
                "name": "Auth_User"
            }
        ]
      },
      ...
    ]
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.