Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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.

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
82 changes: 79 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,88 @@ 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}

Comment thread
auvipy marked this conversation as resolved.
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