Django-rest-framework: 嵌套序列化程序不接收视图上下文

创建于 2015-02-13  ·  26评论  ·  资料来源: encode/django-rest-framework

_这不是 IMO 的 stackoverflow 问题,如果是,请原谅_

我正在尝试使用嵌套序列化程序,我知道这很难讨论,因为我们在 v2-branch 中遇到了很多问题。 我希望嵌套序列化程序字段根据所选项目使用默认值。

项目 ID 在QUERY_PARAMS中提供,并像现在一样在perform_create中应用,在 v3-branch 中。 但是我需要在保存之前在一些嵌套的序列化器字段中进行项目 - 我想为序列化器元数据中的对象提供默认值,因此用户将能够更改一些内容,这些内容可以选择从项目对象中获取。

所以,我用项目对象在get_serializer_context中填充context 。 我应该在嵌套序列化程序中重写__init__来设置默认值,是吗?

问题是,嵌套序列化程序在 __init__ 中既不接收context也不接收parent __init__ 。 看来,它根本没有收到上下文! 因此,它应该是自pull-request 497以来的回归。

我认为,这不是回归,更像是设计更改,但我真的需要上下文才能改变 serizliser 的行为!

最有用的评论

我想一种解决方法可能是始终在 ParentSerializer 的__init__方法中而不是在字段声明中实例化嵌套的ChildSerializer ParentSerializer然后可以手动转发上下文。

像这样:

class ChildSerializer(serializers.Serializer):
    ...

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

这似乎在初始测试中有效。 不确定这是否有任何问题。

所有26条评论

上下文没有(不能)在初始化时传递给字段(或嵌套序列化程序),因为它们在父序列化程序上声明时被初始化......

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

最好是在MySerializer.__init__中执行任何类似的行为,例如...

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

那时你_do_ 可以访问上下文。

如果您真的想要,您可以在字段绑定到其父级时执行一些操作...

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

这应该被视为私有 API,并且应该首选上面列出的父级__init__样式。

是的,我明白你的解释。 这就是为什么我认为这有点棘手。 这就是为什么我的解决方法是在父级的__ini__中创建嵌套序列化程序。 但是嵌套序列化程序__init__在每个新的序列化程序实例创建时调用一次,因为它使用 deepcopy,但我们不能在其中偷偷父实例......
因此,正如我在 v2 中看到的,字段具有initialize方法,该方法非常适合初始化默认值,基于父序列化程序。

所以,似乎bind应该不错。

这应该被视为私有 API,并且应该首选上面列出的父级__init__样式。

但是由于我们不能使用__init__ ,因为我们无法访问当前由视图创​​建的所有可用环境,所以我们应该有公共 API 方法,当我们拥有所有东西时,它会在 field(serializer) 上调用可用的。 就像initialize一样。

它适用于 DRF 2 吗?

可惜上下文不再传播到嵌套的序列化器。

可惜上下文不再传播到嵌套的序列化器。

你是在哪里拿到的 ? Afaik,上下文 _is_ 传播到嵌套的序列化程序。

@xordoquy这很奇怪——我已经针对最新的 drf 做了一个测试用例,但我无法重现问题:) 而且我无法用我自己的代码重现它。 问题解决了。

@xiaohanyu其实我又发现了这个问题。 这有点奇怪:

>>> 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很抱歉打扰,但也许您会有任何想法,这怎么可能?

与此同时,我不得不使用这个 hack 来解决一个问题:

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如果没有简单的测试用例,我们无能为力。 您正在使用几个不同的序列化程序,它们可能有一些用户定义的代码会破坏上下文传播。

事实证明,这是由于复杂的mro发生的,其中多个类是 serializers.Serializer 的子类。 类似的东西

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

无论如何,我认为这可能被视为本地问题..

感谢反馈👍

@xordoquy @tomchristie实际上问题变得非常奇怪:) 每当您在覆盖序列化程序中访问 self.context 时出现缺少上下文的问题。 我试图在fields财产中做到这一点,但没有运气。 这是一个测试用例:

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}

不过这个会通过。 恕我直言,应该在来源中指出,在覆盖方法中访问上下文可能有问题......

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}

需要明确的是——我在这里所做的是根据上下文改变字段的行为。

UPD:在某些情况下,即使最后的代码也不可靠。 事实证明,触摸上下文是危险的。

不幸的是__init__无法访问上下文。 如果想根据上下文中的某些内容在序列化程序中动态创建字段,你会在哪里做呢?

正如上面建议的那样,它似乎在bind中不起作用。 例如self.fields['asdf'] = serializers.CharField() 。 我假设在调用bind时已经评估了这些字段。

fields属性中执行它似乎也不是很好,因为那个东西被调用了很多,并且由于惰性评估它在self._fields中有一个缓存机制,必须被复制在具体的序列化程序中。 最好能够在内部缓存之前修改一次字段。

我认为如果嵌套是作为一个字段完成的,而不是直接调用序列化程序,这不会是一个问题。

像这样:

class ChildSerializer(serializers.Serializer):
    ...

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

然后ChildSerializer可以访问__init__中的上下文,因为它没有在ParentSerializer的字段声明中直接实例化。

如果想根据上下文中的某些内容在序列化程序中动态创建字段,你会在哪里做呢?

让我们结合一个具体的例子来讨论这个问题。 想出一个很好的简单用例,我们可以找出解决它的最佳方法。

我认为如果嵌套是作为一个字段完成的,而不是直接调用序列化程序,这不会是一个问题。

试试看,虽然我不认为我们会走那条路。 这是我们目前所做的一个重大突破,而且它不允许您在子序列化程序上设置参数。 我认为这种风格是有道理的,但我并没有特别看到我们在这个时候发生了变化。

@tomchristie感谢您的回复。

我现在的一个用例是根据当前登录用户的属性在嵌套子序列化程序中的两组不同字段之间切换。

A 类用户不应该看到与 B 类用户相关的字段,反之亦然。

它看起来像这样:

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

但我经常使用 drf 序列化程序和/或 django 表单以多种不同方式修改字段,包括 api 和 html 表单。 动态添加字段,删除字段,修改属性,上面的例子远非唯一的方法。 我以前从来没有在嵌套的孩子身上做过,完全被这个难住了。

@tomchristie我的典型用例是根据上下文控制是否需要字段(想象一下用户和管理员的序列化程序,其中一个 od 他们不需要提供用户 ID,因为它是通过上下文传递的。)现在我被迫使用我之前在此线程中的帖子中的解决方案。

我想一种解决方法可能是始终在 ParentSerializer 的__init__方法中而不是在字段声明中实例化嵌套的ChildSerializer ParentSerializer然后可以手动转发上下文。

像这样:

class ChildSerializer(serializers.Serializer):
    ...

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

这似乎在初始测试中有效。 不确定这是否有任何问题。

是的,我会提出同样的建议。

好吧,在某些情况下它对我不起作用。 可能是因为多重继承。

2016 年 10 月 12 日星期三下午 2:56 +0200,“Tom Christie” [email protected]写道:

是的,我会提出同样的建议。


你收到这个是因为你被提到了。
直接回复此电子邮件,在 GitHub 上查看它,或将线程静音。

我遇到了同样的问题,当我将default属性(在我的情况下CurrentUserDefault()值)设置为(嵌套)序列化程序字段时,该字段具有子类__init__处理方法self.context属性, KeyError出现表明在上下文中找不到request (它不包含任何内容,即它没有从父级传递)。
示例代码:

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

错误日志:

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'

添加的字段缺少序列化程序通过元类所做的一些事情。 由您来设置它们。

正如我上面已经说过的 - 一旦我删除了__init__方法中的上下文检索,一切正常......所以字段设置当然不是问题,因为上下文将从父级传递案子。

对,在我的手机上,没注意到那部分。
然后这是正常的,因为嵌套序列化器在顶部序列化器之前被实例化。
上下文的目的确实是在实例化发生后很长时间才传递它。 所以难怪它在init中不起作用。

好的。 最后一点,我只想指出,就像错误日志显示的那样,它没有出现在__init__方法中,而是出现在CurrentUserDefault()对象中(在它的set_context方法)。 但是如果直接调用上下文参数,即不检查它的存在,它肯定也会出现在__init__中(因为整个上下文是空白的,因为它没有被传递)。
另外,我想重复一下其他人在上面得出的结论——在没有嵌套的情况下(并且在实例化方法中使用上下文时),一切都按预期工作。

向孩子传播上下文时出现问题,应该在https://github.com/encode/django-rest-framework/pull/5304中解决我想应该有很多与缓存None相关的问题

我最近遇到了这个问题。 这是对我有用的解决方案:

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

在上面,我创建了一个序列化器基类,它覆盖get_fields()并将self._context传递给具有相同基类的任何子序列化器。 对于 ListSerializers,我将上下文附加到它的子级。

然后,我检查查询参数“omit_data”并在需要时删除“数据”字段。

我希望这对仍在寻找答案的人有所帮助。

我知道这是一个旧线程,但我只是想分享一下我使用 __init__ 的组合并绑定到我的自定义 mixin 以根据请求(在上下文中)过滤查询集。 此 mixin 假定查询集上存在 get_objects_for_user 方法,否则使用 Guardian.shortcuts.get_objects_for_user 方法。 它对嵌套序列化程序非常有用,在 POST 或 PATCHing 时也是如此。

此页面是否有帮助?
0 / 5 - 0 等级