diff --git a/docs/api-guide/serializers.md b/docs/api-guide/serializers.md index 7ee060af4..c4d6501a2 100644 --- a/docs/api-guide/serializers.md +++ b/docs/api-guide/serializers.md @@ -105,7 +105,9 @@ When deserializing data, we can either create a new instance, or update an exist serializer = CommentSerializer(data=data) # Create new instance serializer = CommentSerializer(comment, data=data) # Update `comment` -By default, serializers must be passed values for all required fields or they will throw validation errors. You can use the `partial` argument in order to allow partial updates. +When creating a new instance, serializers must be passed values for all required fields or they will throw validation errors. + +When updating an existing instance, serializers must be passed values for every field that does not have a default specified, or they will throw validation errors. Omitted fields will be reset to their default values. You can use the `partial` argument in order to allow partial updates, in which case omitted fields will not be changed. serializer = CommentSerializer(comment, data={'content': u'foo bar'}, partial=True) # Update `comment` with partial data diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 68b956822..953ec2cad 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -356,7 +356,8 @@ class WritableField(Field): # Note: partial updates shouldn't set defaults native = self.get_default_value() else: - if self.required: + incomplete_update = self.root.object is not None and not self.partial + if self.required or incomplete_update: raise ValidationError(self.error_messages['required']) return diff --git a/rest_framework/tests/models.py b/rest_framework/tests/models.py index bf9883123..615b43b04 100644 --- a/rest_framework/tests/models.py +++ b/rest_framework/tests/models.py @@ -43,8 +43,10 @@ class SlugBasedModel(RESTFrameworkModel): class DefaultValueModel(RESTFrameworkModel): + required_field = models.CharField(max_length=100) text = models.CharField(default='foobar', max_length=100) extra = models.CharField(blank=True, null=True, max_length=100) + extra_not_nullable = models.CharField(blank=True, max_length=100) class CallableDefaultValueModel(RESTFrameworkModel): diff --git a/rest_framework/tests/test_serializer.py b/rest_framework/tests/test_serializer.py index 198c269f0..c02e03988 100644 --- a/rest_framework/tests/test_serializer.py +++ b/rest_framework/tests/test_serializer.py @@ -867,37 +867,110 @@ class DefaultValueTests(TestCase): self.objects = DefaultValueModel.objects def test_create_using_default(self): - data = {} + data = {'required_field': 'required_field_value'} serializer = self.serializer_class(data=data) self.assertEqual(serializer.is_valid(), True) instance = serializer.save() self.assertEqual(len(self.objects.all()), 1) self.assertEqual(instance.pk, 1) self.assertEqual(instance.text, 'foobar') + self.assertEqual(instance.required_field, 'required_field_value') + self.assertEqual(instance.extra, None) + self.assertEqual(instance.extra_not_nullable, '') def test_create_overriding_default(self): - data = {'text': 'overridden'} + data = {'required_field': 'required_field_value', 'text': 'overridden'} serializer = self.serializer_class(data=data) self.assertEqual(serializer.is_valid(), True) instance = serializer.save() self.assertEqual(len(self.objects.all()), 1) self.assertEqual(instance.pk, 1) self.assertEqual(instance.text, 'overridden') + self.assertEqual(instance.required_field, 'required_field_value') + self.assertEqual(instance.extra, None) + self.assertEqual(instance.extra_not_nullable, '') def test_partial_update_default(self): - """ Regression test for issue #532 """ - data = {'text': 'overridden'} + """ + Regression test for issue #532. Ensure a partial update does not modify omitted + fields to their default values. + """ + data = { + 'required_field': 'required_field_value', + 'text': 'overridden', + 'extra': 'extra_value', + 'extra_not_nullable': 'extra_not_nullable_value', + } serializer = self.serializer_class(data=data, partial=True) self.assertEqual(serializer.is_valid(), True) instance = serializer.save() + self.assertEqual(instance.required_field, 'required_field_value') + self.assertEqual(instance.text, 'overridden') + self.assertEqual(instance.extra, 'extra_value') + self.assertEqual(instance.extra_not_nullable, 'extra_not_nullable_value') - data = {'extra': 'extra_value'} + data = {'extra': 'extra_updated'} serializer = self.serializer_class(instance=instance, data=data, partial=True) self.assertEqual(serializer.is_valid(), True) instance = serializer.save() - - self.assertEqual(instance.extra, 'extra_value') + self.assertEqual(instance.extra, 'extra_updated') self.assertEqual(instance.text, 'overridden') + self.assertEqual(instance.required_field, 'required_field_value') + self.assertEqual(instance.extra_not_nullable, 'extra_not_nullable_value') + + def test_non_partial_update_default(self): + """ + Omitted values in a full update should be reset. + """ + data = { + 'required_field': 'required_field_value', + 'text': 'overridden', + 'extra': 'extra_value', + 'extra_not_nullable': 'extra_not_nullable_value', + } + serializer = self.serializer_class(data=data) + self.assertTrue(serializer.is_valid()) + instance = serializer.save() + self.assertEqual(instance.required_field, 'required_field_value') + self.assertEqual(instance.text, 'overridden') + self.assertEqual(instance.extra, 'extra_value') + self.assertEqual(instance.extra_not_nullable, 'extra_not_nullable_value') + + # Omitting a required field is not valid. + data = {'text': 'text_updated'} + serializer = self.serializer_class(instance=instance, data=data) + self.assertFalse(serializer.is_valid()) + + # Omitting an optional field with no default value is not valid + data = { + 'text': 'text_updated', + 'required_field': 'required_field_updated', + 'extra': 'extra_updated', + } + serializer = self.serializer_class(instance=instance, data=data) + self.assertFalse(serializer.is_valid()) + + data = { + 'text': 'text_updated', + 'required_field': 'required_field_updated', + 'extra_not_nullable': 'extra_not_nullable_updated', + } + serializer = self.serializer_class(instance=instance, data=data) + self.assertFalse(serializer.is_valid()) + + # A field with a default value should be reset to default when omitted + data = { + 'required_field': 'required_field_updated', + 'extra': 'extra_updated', + 'extra_not_nullable': 'extra_not_nullable_updated', + } + serializer = self.serializer_class(instance=instance, data=data) + self.assertTrue(serializer.is_valid()) + instance = serializer.save() + self.assertEqual(instance.text, 'foobar') + self.assertEqual(instance.required_field, 'required_field_updated') + self.assertEqual(instance.extra, 'extra_updated') + self.assertEqual(instance.extra_not_nullable, 'extra_not_nullable_updated') class WritableFieldDefaultValueTests(TestCase):