From f0b06c2081e8c4e790ff3dee87cc875d67c8c1be Mon Sep 17 00:00:00 2001 From: Mateus Garcia de Souza Vieira Date: Fri, 3 Jul 2026 12:11:08 -0300 Subject: [PATCH 1/2] Propagate model field defaults to ModelSerializer fields --- docs/api-guide/fields.md | 2 ++ rest_framework/utils/field_mapping.py | 3 +++ tests/test_model_serializer.py | 34 ++++++++++++++++++++++++--- 3 files changed, 36 insertions(+), 3 deletions(-) diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index 63dbd8b9e9..e99a3d68a1 100644 --- a/docs/api-guide/fields.md +++ b/docs/api-guide/fields.md @@ -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: diff --git a/rest_framework/utils/field_mapping.py b/rest_framework/utils/field_mapping.py index d35caca0c7..f26b2df71c 100644 --- a/rest_framework/utils/field_mapping.py +++ b/rest_framework/utils/field_mapping.py @@ -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 + if model_field.blank and (isinstance(model_field, (models.CharField, models.TextField))): kwargs['allow_blank'] = True diff --git a/tests/test_model_serializer.py b/tests/test_model_serializer.py index 0e291a2de8..37dd8acb97 100644 --- a/tests/test_model_serializer.py +++ b/tests/test_model_serializer.py @@ -176,7 +176,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=\[\]\) date_field = DateField\(\) @@ -185,7 +185,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\) @@ -212,13 +212,41 @@ 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_nullable_boolean_field_choices(self): class NullableBooleanChoicesModel(models.Model): CHECKLIST_OPTIONS = ( From f632454cb9f40fb42eedc07ee025996670999fbb Mon Sep 17 00:00:00 2001 From: Mateus Garcia de Souza Vieira Date: Fri, 3 Jul 2026 12:11:08 -0300 Subject: [PATCH 2/2] Include non-callable field defaults in OPTIONS metadata --- rest_framework/metadata.py | 5 +++++ tests/test_metadata.py | 10 ++++++++++ 2 files changed, 15 insertions(+) diff --git a/rest_framework/metadata.py b/rest_framework/metadata.py index 364ca5b14d..b120517c40 100644 --- a/rest_framework/metadata.py +++ b/rest_framework/metadata.py @@ -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 @@ -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): diff --git a/tests/test_metadata.py b/tests/test_metadata.py index 1bdc8697c4..a7ce2a2630 100644 --- a/tests/test_metadata.py +++ b/tests/test_metadata.py @@ -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):