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/7] 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/7] 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): From 4c81c3478a7b3196c0dd6f9a8dec873cb2d116b7 Mon Sep 17 00:00:00 2001 From: M2004GV Date: Sat, 4 Jul 2026 18:52:05 -0300 Subject: [PATCH 3/7] test: add callable and choice field default tests for ModelSerializer --- tests/test_model_serializer.py | 48 ++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/tests/test_model_serializer.py b/tests/test_model_serializer.py index 37dd8acb97..0cc43bc989 100644 --- a/tests/test_model_serializer.py +++ b/tests/test_model_serializer.py @@ -11,6 +11,7 @@ import re import sys import tempfile +import uuid import pytest from django.core.exceptions import ImproperlyConfigured @@ -247,6 +248,53 @@ class Meta: assert serializer.is_valid(), serializer.errors assert serializer.validated_data == {'value_limit_field': 6} + 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 = ( From 1abd73cbb737b3efa9749a6fe66a1db5a09526c4 Mon Sep 17 00:00:00 2001 From: M2004GV Date: Sat, 4 Jul 2026 18:52:05 -0300 Subject: [PATCH 4/7] test: verify model field defaults appear in OpenAPI schema --- tests/schemas/test_openapi.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/schemas/test_openapi.py b/tests/schemas/test_openapi.py index 98ea2d3d9f..e750d562de 100644 --- a/tests/schemas/test_openapi.py +++ b/tests/schemas/test_openapi.py @@ -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()) From 887edde5fdc64a2927a187dde87faa35bf425023 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Asif=20Saif=20Uddin=20=7B=22Auvi=22=3A=22=E0=A6=85?= =?UTF-8?q?=E0=A6=AD=E0=A6=BF=22=7D?= Date: Sun, 5 Jul 2026 15:39:31 +0600 Subject: [PATCH 5/7] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- tests/test_model_serializer.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/test_model_serializer.py b/tests/test_model_serializer.py index 0cc43bc989..c72722ba9d 100644 --- a/tests/test_model_serializer.py +++ b/tests/test_model_serializer.py @@ -248,6 +248,19 @@ class Meta: 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_callable_default_executed_on_validation(self): """ Callable model defaults (e.g. `uuid.uuid4`) should be propagated as From 961f8c7a37ad9138f4e36cf341878411fa53b75b Mon Sep 17 00:00:00 2001 From: M2004GV Date: Sun, 5 Jul 2026 12:05:26 -0300 Subject: [PATCH 6/7] docs: clarify default propagation excludes relational fields --- docs/api-guide/fields.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index e99a3d68a1..c90b8c083a 100644 --- a/docs/api-guide/fields.md +++ b/docs/api-guide/fields.md @@ -47,7 +47,7 @@ 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. +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. From ffe17a599299a3fff0dd2a79978673833604ed57 Mon Sep 17 00:00:00 2001 From: M2004GV Date: Sun, 5 Jul 2026 12:05:26 -0300 Subject: [PATCH 7/7] test: cover relational field scope for propagated defaults --- tests/test_model_serializer.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/test_model_serializer.py b/tests/test_model_serializer.py index c72722ba9d..811917d069 100644 --- a/tests/test_model_serializer.py +++ b/tests/test_model_serializer.py @@ -261,6 +261,31 @@ class Meta: 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