#36786: XML serializer mishandles nullable elements of a related object's
natural
key
-------------------------------------+-------------------------------------
Reporter: Jacob Walls | Owner: (none)
Type: Bug | Status: new
Component: Core | Version: 5.2
(Serialization) |
Severity: Normal | Resolution:
Keywords: xml | Triage Stage:
| Unreviewed
Has patch: 0 | Needs documentation: 0
Needs tests: 0 | Patch needs improvement: 0
Easy pickings: 0 | UI/UX: 0
-------------------------------------+-------------------------------------
Description changed by Jacob Walls:
Old description:
> 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, foo):
> self.get(foo=foo)
>
> class Widget(models.Model):
> foo = models.UUIDField(null=True)
>
> objects = WidgetManager()
>
> def natural_key(self):
> return (self.foo,)
> }}}
> {{{
> ./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.']
> }}}
New description:
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.']
}}}
--
--
Ticket URL: <https://code.djangoproject.com/ticket/36786#comment:1>
Django <https://code.djangoproject.com/>
The Web framework for perfectionists with deadlines.
--
You received this message because you are subscribed to the Google Groups
"Django updates" group.
To unsubscribe from this group and stop receiving emails from it, send an email
to [email protected].
To view this discussion visit
https://groups.google.com/d/msgid/django-updates/0107019affc96308-886fe2da-cdc5-4a23-962c-9e6574210744-000000%40eu-central-1.amazonses.com.