Skip to content

Fix default value handling for dotted sources #5375

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Aug 31, 2017

Conversation

rpkilby
Copy link
Member

@rpkilby rpkilby commented Aug 30, 2017

This fixes #5371. In short, default handling works as expected when traversed attributes return an object/dict, but not when one part of the traversal returns None.

class Serializer(serializers.Serializer):
    bar = serializers.CharField(default='x', source='foo.bar')
    
>>> Serializer({}).data
ReturnDict([('bar', 'x')])

>>> Serializer({'foo': {}}).data
ReturnDict([('bar', 'x')])

>>> Serializer({'foo': None}).data
ReturnDict([('bar', None)])

@carltongibson carltongibson added this to the 3.6.5 Release milestone Aug 30, 2017
@carltongibson
Copy link
Collaborator

This looks great.

It’s obviously too late for me, why does moving the guard clause fix the issue?

@rpkilby
Copy link
Member Author

rpkilby commented Aug 30, 2017

On second thought, the guard is unneeded.

@rpkilby rpkilby force-pushed the related-source-serialization branch from 44aa0e3 to 07258ca Compare August 30, 2017 21:42
@rpkilby
Copy link
Member Author

rpkilby commented Aug 30, 2017

I had originally just moved the guard, as I thought it was possible for the instance to be None. However, that is never the case due to the following:

  • Serialization implies that the incoming instance cannot be None (here).
  • The get_attribute() function is only invoked through serialization (here).

Given the above, the instance can only be set to None when iterating over the attrs list. With the guard, None is then returned. Without the guard, a subsequent attr would cause an AttributeError to be raised instead, allowing the default to be used.

# with guard
>>> get_attribute({'a': {'b': None}}, ['a', 'b', 'c'])
None

# without guard
>>> get_attribute({'a': {'b': None}}, ['a', 'b', 'c'])
Traceback (most recent call last):
  File "<console>", line 1, in <module>
  File "/Users/bagel/Documents/django-rest-framework/rest_framework/fields.py", line 100, in get_attribute
    instance = getattr(instance, attr)
AttributeError: 'NoneType' object has no attribute 'c'

Copy link
Collaborator

@carltongibson carltongibson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good.

Thanks for taking the time to step through the logic!
(It gets gnarly in there. 😀)

@carltongibson carltongibson merged commit bafb3ec into encode:master Aug 31, 2017
@rpkilby rpkilby deleted the related-source-serialization branch August 31, 2017 07:57
@xordoquy
Copy link
Collaborator

xordoquy commented Oct 9, 2017

This has caused a lot of my builds to go red as the expected keys are now missing.

@rpkilby
Copy link
Member Author

rpkilby commented Oct 9, 2017

Adding default=None to the serializer field should provide the original behavior.

@xordoquy
Copy link
Collaborator

xordoquy commented Oct 9, 2017

I don't think it'll be enough in the case of nested serializers where I get an AttributeError with 3.7.
Currently investigating.

@carltongibson
Copy link
Collaborator

Hey folks. Just FYI, I'm looking at rolling a 3.7.1 this week (≈) so we can get a change/revert in here if we need to.

@rpkilby
Copy link
Member Author

rpkilby commented Oct 9, 2017

I'm in the process of improving the docs and adding more tests. The below passes:

    def test_default_for_multiple_dotted_source(self):
        class Serializer(serializers.Serializer):
            c = serializers.CharField(default='x', source='a.b.c')

        assert Serializer({}).data == {'c': 'x'}

        assert Serializer({'a': {}}).data == {'c': 'x'}
        assert Serializer({'a': None}).data == {'c': 'x'}

        assert Serializer({'a': {'b': {}}}).data == {'c': 'x'}
        assert Serializer({'a': {'b': None}}).data == {'c': 'x'}

        assert Serializer({'a': {'b': {'c': 'abc'}}}).data == {'c': 'abc'}

    def test_default_for_nested_serializer(self):
        class NestedSerializer(serializers.Serializer):
            a = serializers.CharField(default='1')
            c = serializers.CharField(default='2', source='b.c')

        class Serializer(serializers.Serializer):
            nested = NestedSerializer()

        assert Serializer({'nested': None}).data == {'nested': None}
        assert Serializer({'nested': {}}).data == {'nested': {'a': '1', 'c': '2'}}
        assert Serializer({'nested': {'a': '3', 'b': {}}}).data == {'nested': {'a': '3', 'c': '2'}}
        assert Serializer({'nested': {'a': '3', 'b': {'c': '4'}}}).data == {'nested': {'a': '3', 'c': '4'}}

@xordoquy
Copy link
Collaborator

xordoquy commented Oct 9, 2017

yup, I failed to reproduce with regular serializers, but I'm having the issue with ModelSerializer which requires a bit more setup.

carltongibson added a commit to carltongibson/django-rest-framework that referenced this pull request Mar 16, 2018
carltongibson added a commit to carltongibson/django-rest-framework that referenced this pull request Mar 16, 2018
carltongibson added a commit that referenced this pull request Mar 16, 2018
… when path components may be null.

Ref #5375, #5727
pchiquet pushed a commit to pchiquet/django-rest-framework that referenced this pull request Nov 17, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

dotted source on CharField returns null from to_representation when allow_null=False
5 participants