﻿id	summary	reporter	owner	description	type	status	component	version	severity	resolution	keywords	cc	stage	has_patch	needs_docs	needs_tests	needs_better_patch	easy	ui_ux
36786	XML serializer mishandles nullable elements of a related object's natural key	Jacob Walls	Youngkwang Yang	"If a field on your model's natural key is nullable, a dumpdata/loaddata roundtrip works in JSON but fails in XML because the XML fixture contains `<natural>None</natural>`, which deserializes to `""None""`, which is != `None`.

Elsewhere there is an `addQuickElement(""None"")` that produces a clear `<None></None>` value, but nothing like that is used for nullable elements of a natural key.

----
models
{{{#!py
from django.db import models


class WidgetManager(models.Manager):
    def get_by_natural_key(self, name, foo):
        return self.get(name=name, foo=foo)


class Widget(models.Model):
    name = models.CharField(default=""default"")
    foo = models.UUIDField(null=True)

    objects = WidgetManager()


    def natural_key(self):
        return (self.name, self.foo)


class Gadget(models.Model):
    widget = models.ForeignKey(Widget, on_delete=models.CASCADE, null=True)
}}}
{{{
./manage.py makemigrations
./manage.py migrate
./manage.py shell
}}}
{{{#!py
Gadget.objects.create(widget=Widget.objects.create())
}}}
{{{
./manage.py dumpdata myapp --format=xml --natural-foreign > fixture.xml
./manage.py loaddata fixture.xml
}}}
Fixture content:
{{{#!xml
<?xml version=""1.0"" encoding=""utf-8""?>
<django-objects version=""1.0"">
    <object model=""myapp.widget"" pk=""1"">
        <field name=""name"" type=""CharField"">default</field>
        <field name=""foo"" type=""UUIDField"">
            <None></None>
        </field>
    </object>
    <object model=""myapp.gadget"" pk=""1"">
        <field name=""widget"" rel=""ManyToOneRel"" to=""myapp.widget"">
            <natural>default</natural>
            <natural>None</natural>
        </field>
    </object>
</django-objects>
}}}

loaddata error:
{{{#!py
Traceback (most recent call last):
  File ""/Users/jwalls/django/django/db/models/fields/__init__.py"", line 2766, in to_python
    return uuid.UUID(**{input_form: value})
           ~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^
  File ""/Library/Frameworks/Python.framework/Versions/3.14/lib/python3.14/uuid.py"", line 219, in __init__
    raise ValueError('badly formed hexadecimal UUID string')
ValueError: badly formed hexadecimal UUID string

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File ""/Users/jwalls/zed/./manage.py"", line 22, in <module>
    main()
    ~~~~^^
  File ""/Users/jwalls/zed/./manage.py"", line 18, in main
    execute_from_command_line(sys.argv)
    ~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^
  File ""/Users/jwalls/django/django/core/management/__init__.py"", line 443, in execute_from_command_line
    utility.execute()
    ~~~~~~~~~~~~~~~^^
  File ""/Users/jwalls/django/django/core/management/__init__.py"", line 437, in execute
    self.fetch_command(subcommand).run_from_argv(self.argv)
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^
  File ""/Users/jwalls/django/django/core/management/base.py"", line 416, in run_from_argv
    self.execute(*args, **cmd_options)
    ~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^
  File ""/Users/jwalls/django/django/core/management/base.py"", line 460, in execute
    output = self.handle(*args, **options)
  File ""/Users/jwalls/django/django/core/management/commands/loaddata.py"", line 103, in handle
    self.loaddata(fixture_labels)
    ~~~~~~~~~~~~~^^^^^^^^^^^^^^^^
  File ""/Users/jwalls/django/django/core/management/commands/loaddata.py"", line 164, in loaddata
    self.load_label(fixture_label)
    ~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^
  File ""/Users/jwalls/django/django/core/management/commands/loaddata.py"", line 252, in load_label
    for obj in objects:
               ^^^^^^^
  File ""/Users/jwalls/django/django/core/serializers/xml_serializer.py"", line 235, in __next__
    return self._handle_object(node)
           ~~~~~~~~~~~~~~~~~~~^^^^^^
  File ""/Users/jwalls/django/django/core/serializers/xml_serializer.py"", line 293, in _handle_object
    value = self._handle_fk_field_node(field_node, field)
  File ""/Users/jwalls/django/django/core/serializers/xml_serializer.py"", line 332, in _handle_fk_field_node
    obj = model._default_manager.db_manager(
        self.db
    ).get_by_natural_key(*field_value)
  File ""/Users/jwalls/zed/myapp/models.py"", line 6, in get_by_natural_key
    return self.get(name=name, foo=foo)
           ~~~~~~~~^^^^^^^^^^^^^^^^^^^^
  File ""/Users/jwalls/django/django/db/models/manager.py"", line 87, in manager_method
    return getattr(self.get_queryset(), name)(*args, **kwargs)
           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^
  File ""/Users/jwalls/django/django/db/models/query.py"", line 625, in get
    clone = self._chain() if self.query.combinator else self.filter(*args, **kwargs)
                                                        ~~~~~~~~~~~^^^^^^^^^^^^^^^^^
  File ""/Users/jwalls/django/django/db/models/query.py"", line 1542, in filter
    return self._filter_or_exclude(False, args, kwargs)
           ~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^
  File ""/Users/jwalls/django/django/db/models/query.py"", line 1560, in _filter_or_exclude
    clone._filter_or_exclude_inplace(negate, args, kwargs)
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^
  File ""/Users/jwalls/django/django/db/models/query.py"", line 1570, in _filter_or_exclude_inplace
    self._query.add_q(Q(*args, **kwargs))
    ~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^
  File ""/Users/jwalls/django/django/db/models/sql/query.py"", line 1671, in add_q
    clause, _ = self._add_q(q_object, can_reuse)
                ~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^
  File ""/Users/jwalls/django/django/db/models/sql/query.py"", line 1703, in _add_q
    child_clause, needed_inner = self.build_filter(
                                 ~~~~~~~~~~~~~~~~~^
        child,
        ^^^^^^
    ...<7 lines>...
        update_join_types=update_join_types,
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    )
    ^
  File ""/Users/jwalls/django/django/db/models/sql/query.py"", line 1613, in build_filter
    condition = self.build_lookup(lookups, col, value)
  File ""/Users/jwalls/django/django/db/models/sql/query.py"", line 1440, in build_lookup
    lookup = lookup_class(lhs, rhs)
  File ""/Users/jwalls/django/django/db/models/lookups.py"", line 35, in __init__
    self.rhs = self.get_prep_lookup()
               ~~~~~~~~~~~~~~~~~~~~^^
  File ""/Users/jwalls/django/django/db/models/lookups.py"", line 391, in get_prep_lookup
    return super().get_prep_lookup()
           ~~~~~~~~~~~~~~~~~~~~~~~^^
  File ""/Users/jwalls/django/django/db/models/lookups.py"", line 93, in get_prep_lookup
    return self.lhs.output_field.get_prep_value(self.rhs)
           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^
  File ""/Users/jwalls/django/django/db/models/fields/__init__.py"", line 2750, in get_prep_value
    return self.to_python(value)
           ~~~~~~~~~~~~~~^^^^^^^
  File ""/Users/jwalls/django/django/db/models/fields/__init__.py"", line 2768, in to_python
    raise exceptions.ValidationError(
    ...<3 lines>...
    )
django.core.exceptions.ValidationError: ['“None” is not a valid UUID.']
}}}
"	Bug	closed	Core (Serialization)	5.2	Normal	fixed	xml	Youngkwang Yang	Ready for checkin	1	0	0	0	0	0
