Django-rest-framework: El serializador anidado no recibe el contexto de vista

Creado en 13 feb. 2015  ·  26Comentarios  ·  Fuente: encode/django-rest-framework

_No es una pregunta de stackoverflow en mi opinión, así que disculpe si lo es_

Estoy tratando de usar serializadores anidados, sé que es difícil de discutir ya que tuvimos muchos problemas en la rama v2. Quiero que el campo del serializador anidado use el valor predeterminado en función del proyecto seleccionado.

El ID del proyecto se proporciona en QUERY_PARAMS y se aplica en perform_create como debería ser ahora, en la rama v3. Pero necesito un proyecto en algún campo de serializador anidado ANTES de guardar. Quiero proporcionar valores predeterminados para el objeto en los metadatos del serializador, para que el usuario pueda cambiar algunas cosas, que opcionalmente se pueden tomar del objeto del proyecto.

Entonces, relleno context en get_serializer_context con el objeto del proyecto. Y debería reescribir __init__ en el serializador anidado para establecer los valores predeterminados, ¿sí?

El problema es que el serializador anidado no recibe context ni parent en __init__ . ¡Parece que no recibe contexto en absoluto! Entonces, debería ser una regresión desde la solicitud de extracción 497 .

Creo que no es una regresión, más bien un cambio de diseño, ¡pero realmente necesito contexto para poder cambiar el comportamiento del serizliser!

Comentario más útil

Supongo que una solución podría ser instanciar siempre el ChildSerializer anidado en el método __init__ del ParentSerializer en lugar de en la declaración de campo, luego se puede reenviar el contexto manualmente.

Me gusta esto:

class ChildSerializer(serializers.Serializer):
    ...

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

Eso parece funcionar en una prueba inicial. No estoy seguro si hay alguna trampa con esto.

Todos 26 comentarios

El contexto no se pasa (no se puede pasar) a los campos (o al serializador anidado) en el punto de inicio, porque se inicializan cuando se declaran en el serializador principal...

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

Lo mejor es realizar cualquier comportamiento como ese en MySerializer.__init__ , por ejemplo...

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

En ese punto, _sí_ tiene acceso al contexto.

Si realmente lo desea, puede realizar algunas acciones en el punto en que el campo se une a su padre...

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

Esto debería considerarse una API privada, y debería preferirse el estilo principal __init__ mencionado anteriormente.

Si, entiendo lo que explicas. Por eso creo que es un poco complicado. Es por eso que mi solución fue crear un serializador anidado en el __ini__ del padre. Pero el serializador anidado __init__ llama una vez que se crea cada nueva instancia del serializador, ya que usa copia profunda, pero no podemos colar la instancia principal en él...
Entonces, como puedo ver en v2, el campo tenía el método initialize , que era perfecto para iniciar los valores predeterminados, según el serializador principal.

Entonces, parece que bind debería ser bueno.

Esto debería considerarse una API privada, y debería preferirse el estilo principal __init__ mencionado anteriormente.

Pero como simplemente no podemos usar __init__ , porque no podemos acceder a todos los entornos disponibles, creados por la vista en este momento, deberíamos tener un método API público, que se llama en el campo (serializador) cuando tenemos todas las cosas disponible. Como initialize era.

¿Funciona para DRF 2?

es una lástima que el contexto ya no se propague a los serializadores anidados.

es una lástima que el contexto ya no se propague a los serializadores anidados.

De dónde sacaste eso ? Afaik, el contexto _is_ se propaga a los serializadores anidados.

@xordoquy eso es extraño: hice un caso de prueba contra el último drf y no puedo reproducir el problema :) además, no puedo reproducirlo contra mi propio código. Problema resuelto.

@xiaohanyu en realidad encontré este problema una vez más. Y es un poco extraño:

>>> 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 disculpa la molestia, pero tal vez tengas alguna idea de cómo es eso posible.

Mientras tanto, me veo obligado a usar este truco para resolver un 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 no podemos ayudar mucho sin un caso de prueba simple. Está utilizando varios serializadores diferentes que pueden tener algún código definido por el usuario que interrumpe la propagación del contexto.

Resulta que sucede debido al complejo mro , donde varias clases son hijos de serializadores.Serializador. Algo como eso

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 todos modos, eso creo que podría considerarse como un problema local ...

Gracias por los comentarios 👍

@xordoquy @tomchristie en realidad el problema se volvió realmente extraño :) cada vez que accede a self.context en un problema de serializador anulado con falta de contexto aparece. He intentado hacer eso en la propiedad fields sin suerte. Aquí hay un caso de prueba:

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}

Sin embargo, este pasaría. En mi humilde opinión, debe tenerse en cuenta en las fuentes que acceder al contexto en métodos anulados puede 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 claros, lo que estoy haciendo aquí es cambiar el comportamiento de los campos según el contexto.

UPD: incluso el último código no es confiable en algunos casos. Resulta que tocar el contexto es peligroso.

Es lamentable que __init__ no tenga acceso al contexto. Si uno quiere crear dinámicamente campos en el serializador en función de algo en el contexto, ¿dónde haría eso?

No parece funcionar en bind como se sugirió anteriormente. Por ejemplo self.fields['asdf'] = serializers.CharField() . Supongo que los campos ya han sido evaluados cuando se llama a bind .

Hacerlo en la propiedad fields tampoco parece muy bueno, ya que esa cosa se llama mucho, y debido a la evaluación perezosa tiene un mecanismo de almacenamiento en caché en self._fields que tendría que duplicarse en el serializador concreto. Sería mejor poder modificar los campos una vez, antes de que se almacenen en caché internamente.

Creo que esto no sería un problema si el anidamiento se hiciera como un campo, en lugar de llamar directamente al serializador.

Me gusta esto:

class ChildSerializer(serializers.Serializer):
    ...

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

Entonces ChildSerializer podría tener acceso al contexto en __init__ , ya que no se crea una instancia directamente en la declaración de campo en ParentSerializer .

Si uno quiere crear dinámicamente campos en el serializador en función de algo en el contexto, ¿dónde haría eso?

Hablemos de esto en el contexto de un ejemplo específico. Proponga un buen caso de uso simple y podemos encontrar la mejor manera de abordarlo.

Creo que esto no sería un problema si el anidamiento se hiciera como un campo, en lugar de llamar directamente al serializador.

Inténtalo, aunque no creo que vayamos por ese camino. Es un gran cambio en lo que hacemos actualmente, además de que no le permite establecer argumentos en el serializador secundario. Creo que hay un caso válido para ese estilo, pero particularmente no veo que cambiemos en este momento.

@tomchristie gracias por la respuesta.

Un caso de uso que tengo en este momento es cambiar entre dos conjuntos diferentes de campos en un serializador secundario anidado según una propiedad del usuario que ha iniciado sesión actualmente.

Los usuarios de tipo A nunca deben ver los campos que son relevantes para los usuarios de tipo B y viceversa.

Se parece a esto:

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

Pero regularmente modifico campos de muchas maneras diferentes, tanto en formularios api como html, usando serializadores drf y/o formularios django. Agregar campos dinámicamente, eliminar campos, modificar atributos, el ejemplo anterior está lejos de ser la única forma. Simplemente nunca antes tuve que hacerlo en un niño anidado, y esto me dejó completamente perplejo.

@tomchristie, mi caso de uso típico es controlar si el campo es obligatorio o no según el contexto (imagine un serializador para el usuario y el administrador, donde uno de ellos no necesita proporcionar una identificación de usuario ya que se pasa a través del contexto). ahora estoy obligado a use una solución de mis publicaciones anteriores en este hilo.

Supongo que una solución podría ser instanciar siempre el ChildSerializer anidado en el método __init__ del ParentSerializer en lugar de en la declaración de campo, luego se puede reenviar el contexto manualmente.

Me gusta esto:

class ChildSerializer(serializers.Serializer):
    ...

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

Eso parece funcionar en una prueba inicial. No estoy seguro si hay alguna trampa con esto.

Iba a sugerir lo mismo, sí.

Bueno, no funcionó para mí en algunos casos. Probablemente debido a la herencia múltiple.

El miércoles 12 de octubre de 2016 a las 2:56 p. m. +0200, "Tom Christie" [email protected] escribió:

Iba a sugerir lo mismo, sí.


Estás recibiendo esto porque te mencionaron.
Responda a este correo electrónico directamente, véalo en GitHub o silencie el hilo.

Tengo el mismo problema, cuando configuro el atributo default (con CurrentUserDefault() en mi caso) en el campo del serializador (anidado) que ha subclasificado el método __init__ que procesa self.context , el KeyError aparece mostrando que request no se encuentra en contexto (que no contiene nada, es decir, no se pasa del padre).
Código de ejemplo:

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 errores:

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'

El campo agregado carece de algunas cosas que hace el serializador a través de metaclases. Depende de usted establecerlos.

Como ya dije anteriormente, una vez que elimino la recuperación del contexto en el método __init__ , todo funciona bien... Por lo tanto, la configuración del campo ciertamente no es el problema, ya que el contexto se pasará del padre en ese caso.

Cierto, estaba en mi teléfono, no noté esa parte.
Entonces es normal ya que el serializador anidado se instancia mucho antes que el serializador superior.
El propósito del contexto es, de hecho, pasarlo mucho después de que haya ocurrido la instanciación. Así que no es de extrañar que no funcione en init.

Bueno. Para la última nota, solo quiero señalar que, como muestra el registro de errores, no aparece en el método __init__ , sino en el objeto CurrentUserDefault() (en su set_context método). Pero seguro que también aparecería en __init__ si hubiera una llamada directa al parámetro de contexto, es decir, sin verificar su existencia (porque todo el contexto está en blanco, ya que no se pasa por alto).
Además, me gustaría repetir lo que otra persona concluyó anteriormente: en el caso de que no haya anidamiento (y cuando se use el contexto en el método de creación de instancias), todo funciona como se esperaba.

Hubo un problema al propagar el contexto a los niños, debería resolverse en https://github.com/encode/django-rest-framework/pull/5304 Supongo que debería haber muchos problemas relacionados con el almacenamiento en caché None como raíz de serializadores secundarios que deberían resolverse con este PR.

Me encontré con este problema recientemente. Esta es la solución que funcionó para mí:

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

En lo anterior, creo una clase base de serializador que anula get_fields() y pasa self._context a cualquier serializador secundario que tenga la misma clase base. Para ListSerializers, adjunto el contexto al elemento secundario.

Luego, busco un parámetro de consulta "omit_data" y elimino el campo "datos" si se solicita.

Espero que esto sea útil para cualquiera que todavía esté buscando respuestas para esto.

Sé que este es un hilo antiguo, pero solo quería compartir que uso una combinación de __init__ y enlace en mi combinación personalizada para filtrar el conjunto de consultas según la solicitud (en el contexto). Este mixin asume que existe un método get_objects_for_user en el conjunto de consultas, o usa guardian.shortcuts.get_objects_for_user de lo contrario. Funciona muy bien para serializadores anidados, también cuando se hace POST o PATCH.

¿Fue útil esta página
0 / 5 - 0 calificaciones