#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.

Reply via email to