summaryrefslogtreecommitdiff
path: root/django/core
diff options
context:
space:
mode:
authorrimi0108 <hyerimc858@gmail.com>2025-10-04 13:52:59 +0900
committerJacob Walls <jacobtylerwalls@gmail.com>2025-12-03 15:04:52 -0500
commit93540b34d4ef46f68df2c8bfe90447d0f649a852 (patch)
tree93967647a5590ec653372f38cfb7123166d819c3 /django/core
parentddb7236b0ae9be3163c90f799fb79396e9f61cc8 (diff)
Fixed #35729 -- Enabled natural key serialization opt-out for subclasses.
Refactored serialization logic to allow models inheriting a natural_key() method (e.g. AbstractBaseUser) to explicitly opt out of natural key serialization by returning an empty tuple from the method. Thanks Jonas Dittrich for the report. Co-authored-by: Jacob Walls <jacobtylerwalls@gmail.com>
Diffstat (limited to 'django/core')
-rw-r--r--django/core/serializers/base.py28
-rw-r--r--django/core/serializers/python.py21
-rw-r--r--django/core/serializers/xml_serializer.py33
3 files changed, 54 insertions, 28 deletions
diff --git a/django/core/serializers/base.py b/django/core/serializers/base.py
index efc55981eb..cc9caf9a82 100644
--- a/django/core/serializers/base.py
+++ b/django/core/serializers/base.py
@@ -209,6 +209,34 @@ class Serializer:
if callable(getattr(self.stream, "getvalue", None)):
return self.stream.getvalue()
+ def _resolve_natural_key(self, obj):
+ """Return a natural key tuple for the given object when available."""
+ try:
+ return obj.natural_key()
+ except AttributeError:
+ return None
+
+ def _resolve_fk_natural_key(self, obj, field):
+ """
+ Return the natural key for a ForeignKey's related object, or None if
+ not supported.
+ """
+ if not self._model_supports_natural_key(field.remote_field.model):
+ return None
+
+ related = getattr(obj, field.name, None)
+ try:
+ return related.natural_key()
+ except AttributeError:
+ return None
+
+ def _model_supports_natural_key(self, model):
+ """Return True if the model defines a natural_key() method."""
+ try:
+ return callable(model.natural_key)
+ except AttributeError:
+ return False
+
class Deserializer:
"""
diff --git a/django/core/serializers/python.py b/django/core/serializers/python.py
index 2929874b01..53a73e19e5 100644
--- a/django/core/serializers/python.py
+++ b/django/core/serializers/python.py
@@ -34,7 +34,7 @@ class Serializer(base.Serializer):
def get_dump_object(self, obj):
data = {"model": str(obj._meta)}
- if not self.use_natural_primary_keys or not hasattr(obj, "natural_key"):
+ if not self.use_natural_primary_keys or not self._resolve_natural_key(obj):
data["pk"] = self._value_from_field(obj, obj._meta.pk)
data["fields"] = self._current
return data
@@ -52,26 +52,25 @@ class Serializer(base.Serializer):
self._current[field.name] = self._value_from_field(obj, field)
def handle_fk_field(self, obj, field):
- if self.use_natural_foreign_keys and hasattr(
- field.remote_field.model, "natural_key"
+ if self.use_natural_foreign_keys and (
+ natural_key_value := self._resolve_fk_natural_key(obj, field)
):
- related = getattr(obj, field.name)
- if related:
- value = related.natural_key()
- else:
- value = None
+ value = natural_key_value
else:
value = self._value_from_field(obj, field)
self._current[field.name] = value
def handle_m2m_field(self, obj, field):
if field.remote_field.through._meta.auto_created:
- if self.use_natural_foreign_keys and hasattr(
- field.remote_field.model, "natural_key"
+ if self.use_natural_foreign_keys and self._model_supports_natural_key(
+ field.remote_field.model
):
def m2m_value(value):
- return value.natural_key()
+ if natural := value.natural_key():
+ return natural
+ else:
+ return self._value_from_field(value, value._meta.pk)
def queryset_iterator(obj, field):
attr = getattr(obj, field.name)
diff --git a/django/core/serializers/xml_serializer.py b/django/core/serializers/xml_serializer.py
index f8ec0865a7..910c489a37 100644
--- a/django/core/serializers/xml_serializer.py
+++ b/django/core/serializers/xml_serializer.py
@@ -73,7 +73,7 @@ class Serializer(base.Serializer):
self.indent(1)
attrs = {"model": str(obj._meta)}
- if not self.use_natural_primary_keys or not hasattr(obj, "natural_key"):
+ if not self.use_natural_primary_keys or not self._resolve_natural_key(obj):
obj_pk = obj.pk
if obj_pk is not None:
attrs["pk"] = obj._meta.pk.value_to_string(obj)
@@ -128,14 +128,11 @@ class Serializer(base.Serializer):
self._start_relational_field(field)
related_att = getattr(obj, field.attname)
if related_att is not None:
- if self.use_natural_foreign_keys and hasattr(
- field.remote_field.model, "natural_key"
+ if self.use_natural_foreign_keys and (
+ natural_key_value := self._resolve_fk_natural_key(obj, field)
):
- related = getattr(obj, field.name)
- # If related object has a natural key, use it
- related = related.natural_key()
# Iterable natural keys are rolled out as subelements
- for key_value in related:
+ for key_value in natural_key_value:
self.xml.startElement("natural", {})
self.xml.characters(str(key_value))
self.xml.endElement("natural")
@@ -153,19 +150,21 @@ class Serializer(base.Serializer):
"""
if field.remote_field.through._meta.auto_created:
self._start_relational_field(field)
- if self.use_natural_foreign_keys and hasattr(
- field.remote_field.model, "natural_key"
+ if self.use_natural_foreign_keys and self._model_supports_natural_key(
+ field.remote_field.model
):
# If the objects in the m2m have a natural key, use it
def handle_m2m(value):
- natural = value.natural_key()
- # Iterable natural keys are rolled out as subelements
- self.xml.startElement("object", {})
- for key_value in natural:
- self.xml.startElement("natural", {})
- self.xml.characters(str(key_value))
- self.xml.endElement("natural")
- self.xml.endElement("object")
+ if natural := self._resolve_natural_key(value):
+ # Iterable natural keys are rolled out as subelements
+ self.xml.startElement("object", {})
+ for key_value in natural:
+ self.xml.startElement("natural", {})
+ self.xml.characters(str(key_value))
+ self.xml.endElement("natural")
+ self.xml.endElement("object")
+ else:
+ self.xml.addQuickElement("object", attrs={"pk": str(value.pk)})
def queryset_iterator(obj, field):
attr = getattr(obj, field.name)