Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/api-guide/fields.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ If set, this gives the default value that will be used for the field if no input

The `default` is not applied during partial update operations. In the partial update case only fields that are provided in the incoming data will have a validated value returned.

If you're using [Model Serializer](https://www.django-rest-framework.org/api-guide/serializers/#modelserializer) and the corresponding `Model` field declares a `default`, it will be used as the serializer field default. This does not apply to relational fields such as `ForeignKey`: a model default on a relational field is expressed as a primary key value rather than a model instance, so it is not propagated and only causes the generated serializer field to be not required.

May be set to a function or other callable, in which case the value will be evaluated each time it is used. When called, it will receive no arguments. If the callable has a `requires_context = True` attribute, then the serializer field will be passed as an argument.

For example:
Expand Down
5 changes: 5 additions & 0 deletions rest_framework/metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from django.utils.encoding import force_str

from rest_framework import exceptions, serializers
from rest_framework.fields import empty
from rest_framework.request import clone_request
from rest_framework.utils.field_mapping import ClassLookupDict

Expand Down Expand Up @@ -133,6 +134,10 @@ def get_field_info(self, field):
if value is not None and value != '':
field_info[attr] = force_str(value, strings_only=True)

default = getattr(field, 'default', None)
if default is not None and default is not empty and not callable(default):
field_info['default'] = default

if getattr(field, 'child', None):
field_info['child'] = self.get_field_info(field.child)
elif getattr(field, 'fields', None):
Expand Down
3 changes: 3 additions & 0 deletions rest_framework/utils/field_mapping.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,9 @@ def get_field_kwargs(field_name, model_field):
if model_field.has_default() or model_field.blank or model_field.null:
kwargs['required'] = False

if model_field.has_default():
kwargs['default'] = model_field.default

Comment on lines 133 to +138

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

@M2004GV please cross check this suggestion and others relevant ones

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

@auvipy, cross-checked both suggestions. Relational defaults are intentionally not propagated, a FK model default is a PK value, while a serializer field default goes into validated_data as is (no to_internal_value), so propagating it would break .create(). Documented this scope in the docs and locked it in with a test. The full-update test from your autofix is kept as is (plus a missing blank line fix for flake8).

if model_field.blank and (isinstance(model_field, (models.CharField, models.TextField))):
kwargs['allow_blank'] = True

Expand Down
16 changes: 16 additions & 0 deletions tests/schemas/test_openapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,22 @@ class Meta:
assert data['properties']['ro_field']['nullable'], "ro_field nullable must be true"
assert data['properties']['ro_field']['readOnly'], "ro_field read_only must be true"

def test_model_field_default_in_schema(self):
class DefaultsModel(models.Model):
default_field = models.IntegerField(default=0)
callable_default_field = models.UUIDField(default=uuid.uuid4)

class Serializer(serializers.ModelSerializer):
class Meta:
model = DefaultsModel
fields = ["default_field", "callable_default_field"]

inspector = AutoSchema()

data = inspector.map_serializer(Serializer())
assert data['properties']['default_field']['default'] == 0, "model default must appear in schema"
assert 'default' not in data['properties']['callable_default_field'], "callable default must not appear in schema"

def test_primary_key_related_field(self):
class PrimaryKeyRelatedFieldSerializer(serializers.Serializer):
basic = serializers.PrimaryKeyRelatedField(queryset=BasicModel.objects.all())
Expand Down
10 changes: 10 additions & 0 deletions tests/test_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,16 @@ def test_decimal_field_info_type(self):
assert field_info['max_digits'] == 18
assert field_info['decimal_places'] == 4

def test_default_value_field_info(self):
options = metadata.SimpleMetadata()
field_info = options.get_field_info(serializers.IntegerField(default=42))
assert field_info['default'] == 42

def test_callable_default_field_info(self):
options = metadata.SimpleMetadata()
field_info = options.get_field_info(serializers.IntegerField(default=list))
assert 'default' not in field_info


class TestModelSerializerMetadata(TestCase):
def test_read_only_primary_key_related_field(self):
Expand Down
120 changes: 117 additions & 3 deletions tests/test_model_serializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import re
import sys
import tempfile
import uuid

import pytest
from django.core.exceptions import ImproperlyConfigured
Expand Down Expand Up @@ -176,7 +177,7 @@ class Meta:
TestSerializer\(\):
auto_field = IntegerField\(read_only=True\)
big_integer_field = BigIntegerField\(.*\)
boolean_field = BooleanField\(required=False\)
boolean_field = BooleanField\(default=False, required=False\)
char_field = CharField\(max_length=100\)
comma_separated_integer_field = CharField\(max_length=100, validators=\[<django.core.validators.RegexValidator object>\]\)
date_field = DateField\(\)
Expand All @@ -185,7 +186,7 @@ class Meta:
email_field = EmailField\(max_length=100\)
float_field = FloatField\(\)
integer_field = IntegerField\(.*\)
null_boolean_field = BooleanField\(allow_null=True, required=False\)
null_boolean_field = BooleanField\(allow_null=True, default=False, required=False\)
positive_integer_field = IntegerField\(.*\)
positive_small_integer_field = IntegerField\(.*\)
slug_field = SlugField\(allow_unicode=False, max_length=100\)
Expand All @@ -212,13 +213,126 @@ class Meta:
length_limit_field = CharField\(max_length=12, min_length=3\)
blank_field = CharField\(allow_blank=True, max_length=10, required=False\)
null_field = IntegerField\(allow_null=True,.*required=False\)
default_field = IntegerField\(.*required=False\)
default_field = IntegerField\(default=0,.*required=False\)
descriptive_field = IntegerField\(help_text='Some help text', label='A label'.*\)
choices_field = ChoiceField\(choices=(?:\[|\()\('red', 'Red'\), \('blue', 'Blue'\), \('green', 'Green'\)(?:\]|\))\)
text_choices_field = ChoiceField\(choices=(?:\[|\()\('red', 'Red'\), \('blue', 'Blue'\), \('green', 'Green'\)(?:\]|\))\)
""")
assert re.search(expected, repr(TestSerializer())) is not None

def test_default_value_used_when_field_omitted(self):
"""
Model field defaults should be propagated to the serializer field,
so that omitted fields fall back to the model default on validation.
"""
class TestSerializer(serializers.ModelSerializer):
class Meta:
model = FieldOptionsModel
fields = ('value_limit_field', 'default_field')

serializer = TestSerializer(data={'value_limit_field': 5})
assert serializer.is_valid(), serializer.errors
assert serializer.validated_data == {'value_limit_field': 5, 'default_field': 0}

def test_default_value_skipped_on_partial_update(self):
"""
Propagated model defaults should not be applied on partial updates.
"""
class TestSerializer(serializers.ModelSerializer):
class Meta:
model = FieldOptionsModel
fields = ('value_limit_field', 'default_field')

instance = FieldOptionsModel(value_limit_field=5, default_field=10)
serializer = TestSerializer(instance, data={'value_limit_field': 6}, partial=True)
assert serializer.is_valid(), serializer.errors
assert serializer.validated_data == {'value_limit_field': 6}

def test_default_value_used_on_full_update(self):
"""
Propagated model defaults should be applied on non-partial updates.
"""
class TestSerializer(serializers.ModelSerializer):
class Meta:
model = FieldOptionsModel
fields = ('value_limit_field', 'default_field')

instance = FieldOptionsModel(value_limit_field=5, default_field=10)
serializer = TestSerializer(instance, data={'value_limit_field': 6})
assert serializer.is_valid(), serializer.errors
assert serializer.validated_data == {'value_limit_field': 6, 'default_field': 0}

def test_relational_field_default_not_propagated(self):
"""
Model defaults on relational fields are not propagated, since a
serializer field default is used as-is in `validated_data` while a
foreign key model default is expressed as a primary key value
rather than a model instance.
"""
class RelationalDefaultTargetModel(models.Model):
name = models.CharField(max_length=10)

class RelationalDefaultSourceModel(models.Model):
target = models.ForeignKey(
RelationalDefaultTargetModel, on_delete=models.CASCADE, default=1
)

class TestSerializer(serializers.ModelSerializer):
class Meta:
model = RelationalDefaultSourceModel
fields = ('target',)

field = TestSerializer().fields['target']
assert field.default is serializers.empty
assert field.required is False

def test_callable_default_executed_on_validation(self):
"""
Callable model defaults (e.g. `uuid.uuid4`) should be propagated as
callables and evaluated each time the field is omitted from the input.
"""
class CallableDefaultModel(models.Model):
uuid_field = models.UUIDField(default=uuid.uuid4)
name = models.CharField(max_length=10)

class TestSerializer(serializers.ModelSerializer):
class Meta:
model = CallableDefaultModel
fields = ('uuid_field', 'name')

assert TestSerializer().fields['uuid_field'].default is uuid.uuid4

first = TestSerializer(data={'name': 'foo'})
assert first.is_valid(), first.errors
assert isinstance(first.validated_data['uuid_field'], uuid.UUID)

second = TestSerializer(data={'name': 'bar'})
assert second.is_valid(), second.errors
assert second.validated_data['uuid_field'] != first.validated_data['uuid_field']

def test_choices_field_with_default(self):
"""
A choices field with a model default should fall back to that default
and still validate against the choices when omitted (#7469).
"""
class ChoicesDefaultModel(models.Model):
color = models.CharField(max_length=10, choices=COLOR_CHOICES, default='red')

class TestSerializer(serializers.ModelSerializer):
class Meta:
model = ChoicesDefaultModel
fields = ('color',)

assert isinstance(TestSerializer().fields['color'], ChoiceField)

serializer = TestSerializer(data={})
assert serializer.is_valid(), serializer.errors
assert serializer.validated_data == {'color': 'red'}

serializer = TestSerializer(data={'color': 'blue'})
assert serializer.is_valid(), serializer.errors
assert serializer.validated_data == {'color': 'blue'}

def test_nullable_boolean_field_choices(self):
class NullableBooleanChoicesModel(models.Model):
CHECKLIST_OPTIONS = (
Expand Down
Loading