Django-rest-framework: O serializador aninhado não recebe o contexto de visualização

Criado em 13 fev. 2015  ·  26Comentários  ·  Fonte: encode/django-rest-framework

_Não é questão de stackoverflow IMO, então me desculpe se for_

Estou tentando usar serializadores aninhados, sei que é difícil discutir já que tivemos tantos problemas no v2-branch. Eu quero que o campo do serializador aninhado use o padrão com base no projeto selecionado.

ID do projeto fornecido em QUERY_PARAMS e aplicado em perform_create como deveria agora, na ramificação v3. Mas eu preciso do projeto em algum campo do serializador aninhado ANTES de salvar - eu quero fornecer valores padrão para o objeto nos metadados do serializador, para que o usuário possa alterar algumas coisas, que opcionalmente podem ser tiradas do objeto do projeto.

Então, eu preencho context em get_serializer_context com o objeto do projeto. E eu deveria apenas reescrever __init__ no serializador aninhado para definir os padrões, sim?

O problema é que o serializador aninhado não recebe context nem parent em __init__ . Parece que não recebe contexto nenhum! Então, deve ser uma regressão desde pull-request 497 .

Eu acho que não é regressão, mais como mudança de design, mas eu realmente preciso de contexto para poder alterar o comportamento do serizliser!

Comentários muito úteis

Eu acho que uma solução alternativa poderia ser sempre instanciar o ChildSerializer aninhado no método __init__ do ParentSerializer em vez de na declaração do campo, então pode-se encaminhar o contexto manualmente.

Assim:

class ChildSerializer(serializers.Serializer):
    ...

class ParentSerializer(serializers.Serializer):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.fields['child'] = ChildSerializer(context=self.context)

Isso parece funcionar em um teste inicial. Não tenho certeza se há algum problema com isso.

Todos 26 comentários

O contexto não é (não pode ser) passado para os campos (ou serializador aninhado) no ponto de inicialização, porque eles são inicializados quando são declarados no serializador pai...

class MySerializer(Serializer):
    child = ChildSerializer()  <--- We've just initialized this.

O melhor é realizar qualquer comportamento como esse em MySerializer.__init__ , por exemplo...

def __init__(self, *args, **kwargs):
    super(MySerializer, self).__init__(*args, **kwargs)
    self.fields['child'].default = ...

Nesse ponto você _do_ tem acesso ao contexto.

Se você realmente quiser, pode executar algumas ações no ponto em que o campo se tornar vinculado ao seu pai ...

class ChildSerializer(Serializer):
    def bind(self, *args, **kwargs):
        super(ChildSerializer, self).bind(*args, **kwargs)
        # do stuff with self.context.

Isso deve ser considerado uma API privada e o estilo pai __init__ listado acima deve ser preferido.

Sim, eu entendo o que você explica. Por isso acho um pouco complicado. É por isso que minha solução alternativa foi criar um serializador aninhado no pai __ini__ . Mas o serializador aninhado __init__ é chamado assim que cada nova instância do serializador é criada, pois ele usa deepcopy, mas não podemos ocultar a instância pai nele ...
Então, como posso ver na v2, o campo tinha o método initialize , que era perfeito para inicializar os padrões, com base no serializador pai.

Então, parece que bind deve ser bom.

Isso deve ser considerado uma API privada e o estilo pai __init__ listado acima deve ser preferido.

Mas como simplesmente não podemos usar __init__ , pois não podemos acessar todos os ambientes disponíveis, criados pela view no momento, devemos ter o método API público, que é chamado no campo(serializer) quando temos todas as coisas acessível. Como initialize era.

Funciona para DRF 2?

é uma pena que o contexto não seja mais propagado para serializadores aninhados.

é uma pena que o contexto não seja mais propagado para serializadores aninhados.

Onde você conseguiu isso? Afaik, o contexto _is_ propagado para os serializadores aninhados.

@xordoquy isso é estranho - eu fiz um testcase contra o último drf e não consigo reproduzir o problema :) além disso, não consigo reproduzi-lo no meu próprio código. Problema resolvido.

@xiaohanyu , na verdade, encontrei esse problema mais uma vez. E é meio estranho:

>>> serializer
Out[12]: 
# Note there is context passed in constructor
CouponSerializer(<Coupon: Coupon Coupon #0 for offer Offer #0000>, context={'publisher': <User: John ZusPJRryIzYZ>}):
    id = IntegerField(label='ID', read_only=True)
    title = CharField(max_length=100, required=True)
    short_title = CharField(max_length=30)
    offer_details = OfferLimitedSerializer(read_only=True, source='offer'):
        id = IntegerField(label='ID', read_only=True)
        title = CharField(help_text='The Offer Title will be used for the Network, Advertisers and Publishers to identify the specific offer within the application.', read_only=True)       
        status_name = CharField(read_only=True, source='get_status_display')
        is_access_limited = SerializerMethodField()    
    exclusive = BooleanField(required=False)    
    categories_details = OfferCategorySerializer(many=True, read_only=True, source='categories'):
        id = IntegerField(read_only=True)
        category = CharField(read_only=True, source='code')
        category_name = CharField(read_only=True, source='get_code_display')    

# all fields except one got context propagated
>>> [f.field_name for f in serializer.fields.values() if not f.context]
Out[20]: 
['offer_details']

# offer_details is no different than categories_details, both are nested, read only, model serializers..
>>> serializer.fields['categories_details'].context
Out[21]: 
{'publisher': <User: John ZusPJRryIzYZ>}

# lets look inside problematic serializer
>>> offer_details = serializer.fields['offer_details']
>>> offer_details
Out[25]: 

OfferLimitedSerializer(read_only=True, source='offer'):
    id = IntegerField(label='ID', read_only=True)
    title = CharField(help_text='The Offer Title will be used for the Network, Advertisers and Publishers to identify the specific offer within the application.', read_only=True)    
    status_name = CharField(read_only=True, source='get_status_display')
    url = SerializerMethodField()
    is_access_limited = SerializerMethodField()
>>> offer_details.context
Out[23]: 
{}
>>> offer_details._context
Out[24]: 
{}
# ! surprisingly context is available inside fields... but not in the parent
>>> offer_details.fields['is_access_limited'].context
Out[26]: 
{'publisher': <User: John ZusPJRryIzYZ>}

# context is available in all of them!
>>> [x.field_name for x in offer_details.fields.values() if not x.context]
Out[27]: 
[]

@tomchristie desculpe incomodar, mas talvez você tenha alguma ideia de como isso é possível?

Enquanto isso, sou forçado a usar esse hack para resolver um problema:

class Serializer(serializers.Serializer):

    def __init__(self, *args, **kwargs):
        super(Serializer, self).__init__(*args, **kwargs)
        # propagate context to nested complex serializers
        if self.context:
            for field in six.itervalues(self.fields):
                if not field.context:
                    delattr(field, 'context')
                    setattr(field, '_context', self.context)

@pySilver não podemos ajudar muito sem um simples caso de teste. Você está usando vários serializadores diferentes que podem ter algum código definido pelo usuário que interrompe a propagação do contexto.

Acontece que isso acontece devido ao complexo mro , onde várias classes são filhas de serializers.Serializer. Algo parecido

class MyMixin(serializers.Serializer):
   # some code within __init__, calling parent too

class MyModelSerializer(serializers.ModelSerializer):
   # some code within __init__, calling parent too

class MyProblematicSerializer(MyModelSerializer, MyMixin)
    # this one will not receive context somehow.
     pass

de qualquer forma, acho que isso pode ser considerado um problema local ..

Obrigado pelo feedback 👍

@xordoquy @tomchristie, na verdade, o problema ficou muito estranho :) sempre que você acessa self.context no problema do serializador substituído com contexto ausente aparece. Eu tentei fazer isso na propriedade fields sem sorte. Aqui está um caso de teste:

def test_serializer_context(self):
        class MyBaseSerializer(serializers.Serializer):
            def __init__(self, *args, **kwargs):
                super(MyBaseSerializer, self).__init__(*args, **kwargs)
                x = 0
                if self.context.get('x'):
                    x = self.context['x']

        class SubSerializer(MyBaseSerializer):
            char = serializers.CharField()
            integer = serializers.IntegerField()

        class ParentSerializer(MyBaseSerializer):
            with_context = serializers.CharField()
            without_context = SubSerializer()

        serializer = ParentSerializer(data={}, context={'what': 42})
        assert serializer.context == {'what': 42}
        assert serializer.fields['with_context'].context == {'what': 42}
        assert serializer.fields['without_context'].context == {'what': 42}

No entanto, este passaria. imho, deve-se notar nas fontes que acessar o contexto em métodos substituídos pode ser problemático ...

def test_serializer_context(self):
        class MyBaseSerializer(serializers.Serializer):
            <strong i="12">@property</strong>
            def fields(self):
                fields = super(MyBaseSerializer, self).fields
                x = 0
                if self.root.context.get('x'):
                    x = 1
                return fields


        class SubSerializer(MyBaseSerializer):
            char = serializers.CharField()
            integer = serializers.IntegerField()

        class ParentSerializer(MyBaseSerializer):
            with_context = serializers.CharField()
            without_context = SubSerializer()

        serializer = ParentSerializer(data={}, context={'what': 42})
        assert serializer.context == {'what': 42}
        assert serializer.fields['with_context'].context == {'what': 42}
        assert serializer.fields['without_context'].context == {'what': 42}

Para ser claro – o que estou fazendo aqui é mudar o comportamento dos campos com base no contexto.

UPD: mesmo o último código não é confiável em alguns casos. Acontece que tocar o contexto é perigoso.

É lamentável que __init__ não tenha acesso ao contexto. Se alguém quiser criar campos dinamicamente no serializador com base em algo no contexto, onde você faria isso?

Não parece funcionar em bind como sugerido acima. Por exemplo self.fields['asdf'] = serializers.CharField() . Suponho que os campos já foram avaliados quando bind é chamado.

Fazer isso na propriedade fields também não parece ótimo, já que essa coisa se chama muito, e devido à avaliação preguiçosa ela tem um mecanismo de cache em self._fields que teria que ser duplicado no serializador de concreto. Seria melhor poder modificar os campos uma vez, antes de serem armazenados em cache internamente.

Acho que isso não seria um problema se o aninhamento fosse feito como um campo, em vez de chamar o serializador diretamente.

Assim:

class ChildSerializer(serializers.Serializer):
    ...

class ParentSerializer(serializers.Serializer):
    child = serializers.NestedField(serializer=ChildSerializer)

Então o ChildSerializer poderia ter acesso ao contexto em __init__ , já que não está sendo instanciado diretamente na declaração do campo em ParentSerializer .

Se alguém quiser criar campos dinamicamente no serializador com base em algo no contexto, onde você faria isso?

Vamos falar sobre isso no contexto de um exemplo específico. Invente um bom caso de uso simples e podemos descobrir a melhor maneira de lidar com isso.

Acho que isso não seria um problema se o aninhamento fosse feito como um campo, em vez de chamar o serializador diretamente.

Tente, embora eu não ache que provavelmente iremos por esse caminho. É uma grande mudança no que fazemos atualmente, além de não permitir que você defina argumentos no serializador filho. Acho que há um caso válido para esse estilo, mas não nos vejo mudando neste momento.

@tomchristie obrigado pela resposta.

Um caso de uso que tenho agora é alternar entre dois conjuntos diferentes de campos em um serializador filho aninhado, dependendo de uma propriedade do usuário conectado no momento.

Os usuários do Tipo A nunca devem ver os campos relevantes para os usuários do Tipo B e vice-versa.

Se parece com isso:

class ChildSerializer(serializers.Serializer):
  # relevant for all users
  name = serializers.CharField()

  # only relevant for users of Type A
  phone = serializers.CharField()

  # only relevant for users of Type B
  ssn = serializers.CharField()

Mas eu modifico regularmente campos de muitas maneiras diferentes, tanto em formulários de API quanto em html, usando serializadores drf e/ou formulários django. Adicionando campos dinamicamente, removendo campos, modificando atributos, o exemplo acima está longe de ser o único caminho. Eu nunca tive que fazer isso em uma criança aninhada antes, e fiquei completamente perplexo com isso.

@tomchristie meu caso de uso típico é controlar se o campo é obrigatório ou não com base no contexto (imagine um serializador para usuário e administrador, onde um deles não precisa fornecer o ID do usuário, pois é passado via contexto). use uma solução dos meus posts anteriores neste tópico.

Eu acho que uma solução alternativa poderia ser sempre instanciar o ChildSerializer aninhado no método __init__ do ParentSerializer em vez de na declaração do campo, então pode-se encaminhar o contexto manualmente.

Assim:

class ChildSerializer(serializers.Serializer):
    ...

class ParentSerializer(serializers.Serializer):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.fields['child'] = ChildSerializer(context=self.context)

Isso parece funcionar em um teste inicial. Não tenho certeza se há algum problema com isso.

Ia sugerir o mesmo, sim.

Bem, não funcionou para mim para alguns casos. Provavelmente por causa de herança múltipla.

Em quarta-feira, 12 de outubro de 2016 às 14h56 +0200, "Tom Christie" [email protected] escreveu:

Ia sugerir o mesmo, sim.


Você está recebendo isso porque foi mencionado.
Responda a este e-mail diretamente, visualize-o no GitHub ou silencie a conversa.

Estou tendo o mesmo problema, quando defino o atributo default (com CurrentUserDefault() no meu caso) para o campo serializador (aninhado) que subclassificou o método __init__ que processa self.context , o KeyError aparece mostrando que request não é encontrado no contexto (que não contém nada, ou seja, não é passado do pai).
Código de exemplo:

class UserSerializer(serializers.ModelSerializer):
# ...
    def __init__(self, *args, **kwargs):
        kwargs.pop('fields', None)
        super().__init__(*args, **kwargs)
        if 'list' in self.context: # Once I remove this, it will work
            self.fields['friend_count'] = serializers.SerializerMethodField()
# ...

class CommentSerializer(serializers.ModelSerializer):
    person = UserSerializer(read_only=True, default=serializers.CurrentUserDefault())
# ...

Registro de erros:

Environment:


Request Method: POST
Request URL: http://127.0.0.1:8000/api/comments/98/

Django Version: 1.9.7
Python Version: 3.4.3
Installed Applications:
['django.contrib.admin',
 'django.contrib.auth',
 'django.contrib.contenttypes',
 'django.contrib.sessions',
 'django.contrib.messages',
 'django.contrib.staticfiles',
 'django.contrib.sites',
 'rest_framework',
 'rest_framework.authtoken',
 'generic_relations',
 'decorator_include',
 'allauth',
 'allauth.account',
 'allauth.socialaccount',
 'allauth.socialaccount.providers.facebook',
 'allauth.socialaccount.providers.google',
 'phonenumber_field',
 'bootstrap3',
 'stronghold',
 'captcha',
 'django_settings_export',
 'main']
Installed Middleware:
['django.middleware.security.SecurityMiddleware',
 'django.contrib.sessions.middleware.SessionMiddleware',
 'django.middleware.common.CommonMiddleware',
 'django.middleware.csrf.CsrfViewMiddleware',
 'django.contrib.auth.middleware.AuthenticationMiddleware',
 'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
 'django.contrib.messages.middleware.MessageMiddleware',
 'django.middleware.clickjacking.XFrameOptionsMiddleware',
 'stronghold.middleware.LoginRequiredMiddleware']



Traceback:

File "/home/mikisoft/django-dev/lib/python3.4/site-packages/django/core/handlers/base.py" in get_response
  149.                     response = self.process_exception_by_middleware(e, request)

File "/home/mikisoft/django-dev/lib/python3.4/site-packages/django/core/handlers/base.py" in get_response
  147.                     response = wrapped_callback(request, *callback_args, **callback_kwargs)

File "/home/mikisoft/django-dev/lib/python3.4/site-packages/django/views/decorators/csrf.py" in wrapped_view
  58.         return view_func(*args, **kwargs)

File "/home/mikisoft/django-dev/lib/python3.4/site-packages/django/views/generic/base.py" in view
  68.             return self.dispatch(request, *args, **kwargs)

File "/home/mikisoft/django-dev/lib/python3.4/site-packages/rest_framework/views.py" in dispatch
  466.             response = self.handle_exception(exc)

File "/home/mikisoft/django-dev/lib/python3.4/site-packages/rest_framework/views.py" in dispatch
  463.             response = handler(request, *args, **kwargs)

File "/home/mikisoft/django-dev/lib/python3.4/site-packages/rest_framework/generics.py" in post
  246.         return self.create(request, *args, **kwargs)

File "/home/mikisoft/django-dev/lib/python3.4/site-packages/rest_framework/mixins.py" in create
  20.         serializer.is_valid(raise_exception=True)

File "/home/mikisoft/django-dev/lib/python3.4/site-packages/rest_framework/serializers.py" in is_valid
  213.                 self._validated_data = self.run_validation(self.initial_data)

File "/home/mikisoft/django-dev/lib/python3.4/site-packages/rest_framework/serializers.py" in run_validation
  407.         value = self.to_internal_value(data)

File "/home/mikisoft/django-dev/lib/python3.4/site-packages/rest_framework/serializers.py" in to_internal_value
  437.                 validated_value = field.run_validation(primitive_value)

File "/home/mikisoft/django-dev/lib/python3.4/site-packages/rest_framework/serializers.py" in run_validation
  403.         (is_empty_value, data) = self.validate_empty_values(data)

File "/home/mikisoft/django-dev/lib/python3.4/site-packages/rest_framework/fields.py" in validate_empty_values
  453.             return (True, self.get_default())

File "/home/mikisoft/django-dev/lib/python3.4/site-packages/rest_framework/fields.py" in get_default
  437.                 self.default.set_context(self)

File "/home/mikisoft/django-dev/lib/python3.4/site-packages/rest_framework/fields.py" in set_context
  239.         self.user = serializer_field.context['request'].user

Exception Type: KeyError at /api/comments/98/
Exception Value: 'request'

O campo adicionado carece de algumas coisas que o serializador faz por meio de metaclasses. Cabe a você configurá-los.

Como eu já disse acima - uma vez que eu removo a recuperação de contexto no método __init__ , tudo funciona bem ... Então a configuração do campo certamente não é o problema, pois o contexto será passado do pai nesse caso.

Certo, estava no meu telefone, não percebi essa parte.
Então é normal, pois o serializador aninhado é instanciado muito antes do serializador superior.
O propósito do contexto é, de fato, passá-lo muito depois que a instanciação aconteceu. Portanto, não é de admirar que não funcione no init.

OK. Para a última nota, eu só quero salientar que, como mostra o log de erros, ele não aparece no método __init__ , mas no objeto CurrentUserDefault() (em seu set_context método). Mas claro que também apareceria em __init__ se houvesse uma chamada direta ao parâmetro context, ou seja, sem verificar sua existência (porque todo o contexto está em branco, pois não é passado).
Além disso, gostaria de repetir o que foi concluído acima por outra pessoa - no caso em que não há aninhamento (e quando o contexto é usado no método de instanciação), tudo funciona conforme o esperado.

Houve um problema na propagação de contexto para crianças, deve ser resolvido em https://github.com/encode/django-rest-framework/pull/5304 Acho que deve ter havido muitos problemas relacionados ao cache None como raiz dos serializadores filhos que devem ser resolvidos com este PR.

Me deparei com esse problema recentemente. Esta é a solução que funcionou para mim:

class MyBaseSerializer(serializers.HyperlinkedModelSerializer):

    def get_fields(self):
        '''
        Override get_fields() method to pass context to other serializers of this base class.

        If the context contains query param "omit_data" as set to true, omit the "data" field
        '''
        fields = super().get_fields()

        # Cause fields with this same base class to inherit self._context
        for field_name in fields:
            if isinstance(fields[field_name], serializers.ListSerializer):
                if isinstance(fields[field_name].child, MyBaseSerializer):
                    fields[field_name].child._context = self._context

            elif isinstance(fields[field_name], MyBaseSerializer):
                fields[field_name]._context = self._context

        # Check for "omit_data" in the query params and remove data field if true
        if 'request' in self._context:
            omit_data = self._context['request'].query_params.get('omit_data', False)

            if omit_data and omit_data.lower() in ['true', '1']:
                fields.pop('data')

        return fields

Acima, crio uma classe base do serializador que substitui get_fields() e passa self._context para qualquer serializador filho que tenha a mesma classe base. Para ListSerializers, anexe o contexto ao filho dele.

Em seguida, procuro um parâmetro de consulta "omit_data" e removo o campo "data" se for solicitado.

Espero que isso seja útil para quem ainda procura respostas para isso.

Eu sei que este é um tópico antigo, mas eu só queria compartilhar que eu uso uma combinação de __init__ e vinculo meu mixin personalizado para filtrar o conjunto de consultas com base na solicitação (no contexto). Este mixin assume que existe um método get_objects_for_user no queryset, ou usa guardião.shortcuts.get_objects_for_user caso contrário. Funciona muito bem para serializadores aninhados, também ao POSTing ou PATCHing.

Esta página foi útil?
0 / 5 - 0 avaliações