Opened 56 minutes ago

Last modified 10 minutes ago

#36786 new Bug

XML serializer mishandles nullable elements of a related object's natural key

Reported by: Jacob Walls Owned by:
Component: Core (Serialization) Version: 5.2
Severity: Normal Keywords: xml
Cc: Triage Stage: Accepted
Has patch: no Needs documentation: no
Needs tests: no Patch needs improvement: no
Easy pickings: no UI/UX: no

Description (last modified by Jacob Walls)

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

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
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 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:

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.']

Change History (2)

comment:1 by Jacob Walls, 54 minutes ago

Description: modified (diff)

comment:2 by David Smith, 10 minutes ago

Triage Stage: UnreviewedAccepted
Note: See TracTickets for help on using tickets.
Back to Top