From f31b2ffa5c27e4c7f5c2c78a156039bae8d5d874 Mon Sep 17 00:00:00 2001 From: justyou0606 Date: Sun, 14 Jun 2026 10:27:39 +0800 Subject: [PATCH 1/4] Fix duplicate validation errors for GenericIPAddressField with protocol When a Django model uses GenericIPAddressField(protocol='IPv4') or (protocol='IPv6'), the model field gets a protocol-specific validator (validate_ipv4_address or validate_ipv6_address) from Django. DRF's IPAddressField also adds the same validator via ip_address_validators(), resulting in duplicate error messages. Fix: Remove validate_ipv4_address and validate_ipv6_address from the serializer's validator_kwarg, in addition to the already-removed validate_ipv46_address. Fixes #9645 --- rest_framework/utils/field_mapping.py | 6 ++++- tests/test_model_serializer.py | 36 +++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/rest_framework/utils/field_mapping.py b/rest_framework/utils/field_mapping.py index d35caca0c7..6637ce366b 100644 --- a/rest_framework/utils/field_mapping.py +++ b/rest_framework/utils/field_mapping.py @@ -208,7 +208,11 @@ def get_field_kwargs(field_name, model_field): if isinstance(model_field, models.GenericIPAddressField): validator_kwarg = [ validator for validator in validator_kwarg - if validator is not validators.validate_ipv46_address + if validator not in ( + validators.validate_ipv46_address, + validators.validate_ipv4_address, + validators.validate_ipv6_address, + ) ] # Our decimal validation is handled in the field code, not validator code. if isinstance(model_field, models.DecimalField): diff --git a/tests/test_model_serializer.py b/tests/test_model_serializer.py index 0e291a2de8..abb86b78cc 100644 --- a/tests/test_model_serializer.py +++ b/tests/test_model_serializer.py @@ -439,6 +439,42 @@ class Meta: 'Unexpected number of validation errors: ' '{}'.format(s.errors)) + def test_ip_address_validation_with_protocol_ipv4(self): + class IPAddressFieldModel(models.Model): + address = models.GenericIPAddressField(protocol='IPv4') + + class Meta: + app_label = 'test_model_serializer' + + class TestSerializer(serializers.ModelSerializer): + class Meta: + model = IPAddressFieldModel + fields = '__all__' + + s = TestSerializer(data={'address': 'not an ip address'}) + self.assertFalse(s.is_valid()) + self.assertEqual(1, len(s.errors['address']), + 'Unexpected number of validation errors: ' + '{}'.format(s.errors)) + + def test_ip_address_validation_with_protocol_ipv6(self): + class IPAddressFieldModel(models.Model): + address = models.GenericIPAddressField(protocol='IPv6') + + class Meta: + app_label = 'test_model_serializer' + + class TestSerializer(serializers.ModelSerializer): + class Meta: + model = IPAddressFieldModel + fields = '__all__' + + s = TestSerializer(data={'address': 'not an ip address'}) + self.assertFalse(s.is_valid()) + self.assertEqual(1, len(s.errors['address']), + 'Unexpected number of validation errors: ' + '{}'.format(s.errors)) + @pytest.mark.skipif('not postgres_fields') class TestPosgresFieldsMapping(TestCase): From 4cdbee5e61317265a6d2a68f87d0bc32a396cd97 Mon Sep 17 00:00:00 2001 From: Bruno Alla Date: Sat, 25 Jul 2026 18:21:26 +0100 Subject: [PATCH 2/4] Address code review comments --- rest_framework/utils/field_mapping.py | 8 +++-- tests/test_model_serializer.py | 48 +++++++++++++++++++++------ 2 files changed, 43 insertions(+), 13 deletions(-) diff --git a/rest_framework/utils/field_mapping.py b/rest_framework/utils/field_mapping.py index 6637ce366b..91c4432fb1 100644 --- a/rest_framework/utils/field_mapping.py +++ b/rest_framework/utils/field_mapping.py @@ -204,15 +204,17 @@ def get_field_kwargs(field_name, model_field): if validator is not validators.validate_slug ] - # IPAddressField do not need to include the 'validate_ipv46_address' argument, + # IPAddressField does not need to include the IP address validators, + # as it adds its own based on the 'protocol' argument. if isinstance(model_field, models.GenericIPAddressField): + kwargs['protocol'] = model_field.protocol validator_kwarg = [ validator for validator in validator_kwarg - if validator not in ( + if validator not in { validators.validate_ipv46_address, validators.validate_ipv4_address, validators.validate_ipv6_address, - ) + } ] # Our decimal validation is handled in the field code, not validator code. if isinstance(model_field, models.DecimalField): diff --git a/tests/test_model_serializer.py b/tests/test_model_serializer.py index abb86b78cc..1ccbbabd83 100644 --- a/tests/test_model_serializer.py +++ b/tests/test_model_serializer.py @@ -440,41 +440,69 @@ class Meta: '{}'.format(s.errors)) def test_ip_address_validation_with_protocol_ipv4(self): - class IPAddressFieldModel(models.Model): + class IPv4AddressFieldModel(models.Model): address = models.GenericIPAddressField(protocol='IPv4') - class Meta: - app_label = 'test_model_serializer' - class TestSerializer(serializers.ModelSerializer): class Meta: - model = IPAddressFieldModel + model = IPv4AddressFieldModel fields = '__all__' + expected = dedent(""" + TestSerializer(): + id = IntegerField(label='ID', read_only=True) + address = IPAddressField(protocol='IPv4') + """) + self.assertEqual(repr(TestSerializer()), expected) + s = TestSerializer(data={'address': 'not an ip address'}) self.assertFalse(s.is_valid()) self.assertEqual(1, len(s.errors['address']), 'Unexpected number of validation errors: ' '{}'.format(s.errors)) + # An IPv6 address is not valid for an IPv4-only field. + s = TestSerializer(data={'address': '2001:db8::1'}) + self.assertFalse(s.is_valid()) + self.assertEqual(1, len(s.errors['address']), + 'Unexpected number of validation errors: ' + '{}'.format(s.errors)) + + s = TestSerializer(data={'address': '192.0.2.1'}) + self.assertTrue(s.is_valid(), s.errors) + def test_ip_address_validation_with_protocol_ipv6(self): - class IPAddressFieldModel(models.Model): + class IPv6AddressFieldModel(models.Model): address = models.GenericIPAddressField(protocol='IPv6') - class Meta: - app_label = 'test_model_serializer' - class TestSerializer(serializers.ModelSerializer): class Meta: - model = IPAddressFieldModel + model = IPv6AddressFieldModel fields = '__all__' + expected = dedent(""" + TestSerializer(): + id = IntegerField(label='ID', read_only=True) + address = IPAddressField(protocol='IPv6') + """) + self.assertEqual(repr(TestSerializer()), expected) + s = TestSerializer(data={'address': 'not an ip address'}) self.assertFalse(s.is_valid()) self.assertEqual(1, len(s.errors['address']), 'Unexpected number of validation errors: ' '{}'.format(s.errors)) + # An IPv4 address is not valid for an IPv6-only field. + s = TestSerializer(data={'address': '192.0.2.1'}) + self.assertFalse(s.is_valid()) + self.assertEqual(1, len(s.errors['address']), + 'Unexpected number of validation errors: ' + '{}'.format(s.errors)) + + s = TestSerializer(data={'address': '2001:db8::1'}) + self.assertTrue(s.is_valid(), s.errors) + @pytest.mark.skipif('not postgres_fields') class TestPosgresFieldsMapping(TestCase): From 0aa1a8c8d1a06e4a8d549279f86b87c4825ab4f8 Mon Sep 17 00:00:00 2001 From: Bruno Alla Date: Tue, 4 Aug 2026 21:00:11 +0100 Subject: [PATCH 3/4] Extend test coverage to validate error messages too --- tests/test_model_serializer.py | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/tests/test_model_serializer.py b/tests/test_model_serializer.py index 1ccbbabd83..8b34d60b62 100644 --- a/tests/test_model_serializer.py +++ b/tests/test_model_serializer.py @@ -435,9 +435,8 @@ class Meta: s = TestSerializer(data={'address': 'not an ip address'}) self.assertFalse(s.is_valid()) - self.assertEqual(1, len(s.errors['address']), - 'Unexpected number of validation errors: ' - '{}'.format(s.errors)) + self.assertEqual(s.errors['address'], + ['Enter a valid IPv4 or IPv6 address.']) def test_ip_address_validation_with_protocol_ipv4(self): class IPv4AddressFieldModel(models.Model): @@ -457,16 +456,14 @@ class Meta: s = TestSerializer(data={'address': 'not an ip address'}) self.assertFalse(s.is_valid()) - self.assertEqual(1, len(s.errors['address']), - 'Unexpected number of validation errors: ' - '{}'.format(s.errors)) + self.assertEqual(s.errors['address'], + ['Enter a valid IPv4 address.']) # An IPv6 address is not valid for an IPv4-only field. s = TestSerializer(data={'address': '2001:db8::1'}) self.assertFalse(s.is_valid()) - self.assertEqual(1, len(s.errors['address']), - 'Unexpected number of validation errors: ' - '{}'.format(s.errors)) + self.assertEqual(s.errors['address'], + ['Enter a valid IPv4 address.']) s = TestSerializer(data={'address': '192.0.2.1'}) self.assertTrue(s.is_valid(), s.errors) @@ -489,16 +486,14 @@ class Meta: s = TestSerializer(data={'address': 'not an ip address'}) self.assertFalse(s.is_valid()) - self.assertEqual(1, len(s.errors['address']), - 'Unexpected number of validation errors: ' - '{}'.format(s.errors)) + self.assertEqual(s.errors['address'], + ['Enter a valid IPv6 address.']) # An IPv4 address is not valid for an IPv6-only field. s = TestSerializer(data={'address': '192.0.2.1'}) self.assertFalse(s.is_valid()) - self.assertEqual(1, len(s.errors['address']), - 'Unexpected number of validation errors: ' - '{}'.format(s.errors)) + self.assertEqual(s.errors['address'], + ['Enter a valid IPv6 address.']) s = TestSerializer(data={'address': '2001:db8::1'}) self.assertTrue(s.is_valid(), s.errors) From c9f8bc456834502c958e25394362e2132345d2b9 Mon Sep 17 00:00:00 2001 From: Bruno Alla Date: Wed, 5 Aug 2026 08:03:11 +0100 Subject: [PATCH 4/4] Add test with unhashable type validator --- rest_framework/utils/field_mapping.py | 4 ++-- tests/test_model_serializer.py | 25 ++++++++++++++++++++++++- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/rest_framework/utils/field_mapping.py b/rest_framework/utils/field_mapping.py index 91c4432fb1..fd456a08c9 100644 --- a/rest_framework/utils/field_mapping.py +++ b/rest_framework/utils/field_mapping.py @@ -210,11 +210,11 @@ def get_field_kwargs(field_name, model_field): kwargs['protocol'] = model_field.protocol validator_kwarg = [ validator for validator in validator_kwarg - if validator not in { + if validator not in ( validators.validate_ipv46_address, validators.validate_ipv4_address, validators.validate_ipv6_address, - } + ) ] # Our decimal validation is handled in the field code, not validator code. if isinstance(model_field, models.DecimalField): diff --git a/tests/test_model_serializer.py b/tests/test_model_serializer.py index 8b34d60b62..914b932d73 100644 --- a/tests/test_model_serializer.py +++ b/tests/test_model_serializer.py @@ -16,7 +16,8 @@ from django.core.exceptions import ImproperlyConfigured from django.core.serializers.json import DjangoJSONEncoder from django.core.validators import ( - MaxValueValidator, MinLengthValidator, MinValueValidator + MaxLengthValidator, MaxValueValidator, MinLengthValidator, + MinValueValidator ) from django.db import models from django.db.models.signals import m2m_changed @@ -438,6 +439,28 @@ class Meta: self.assertEqual(s.errors['address'], ['Enter a valid IPv4 or IPv6 address.']) + def test_ip_address_validation_with_custom_validator(self): + class IPAddressFieldModel(models.Model): + address = models.GenericIPAddressField( + # MaxLengthValidator is an unhashable type + validators=[MaxLengthValidator(15)], + ) + + class TestSerializer(serializers.ModelSerializer): + class Meta: + model = IPAddressFieldModel + fields = '__all__' + + s = TestSerializer(data={'address': 'not an ip address'}) + self.assertFalse(s.is_valid()) + self.assertEqual( + s.errors['address'], + [ + 'Ensure this value has at most 15 characters (it has 17).', + 'Enter a valid IPv4 or IPv6 address.', + ], + ) + def test_ip_address_validation_with_protocol_ipv4(self): class IPv4AddressFieldModel(models.Model): address = models.GenericIPAddressField(protocol='IPv4')