Repository: tefra/xsdata

Issue:
Loss of Fractional Part in Numbers When Converting Between JSON and Dataclass
First of all, thank you so much for the library; it's a lifesaver :)

I've encountered an issue with the conversion chain from XML -> Dataclass -> JSON, and then back from the resulting JSON to Dataclass, where I noticed that the fractional part of numbers is getting lost.

Here's a short example that demonstrates this behavior:

```python
from dataclasses import dataclass, field
from xsdata.formats.dataclass.parsers import JsonParser
from typing import Optional, Union

parser = JsonParser()

@dataclass
class Account:
    summ_current: Optional[Union[float, int]] = field(
        default=None,
        metadata={
            "type": "Attribute",
            "required": True,
        }
    )

json_str = """{"summ_current": 6353.45}"""

print(parser.from_string(json_str, Account))
# print result: Account(summ_current=6353)
```

As you can see, after parsing the JSON string, the `summ_current` attribute loses its fractional part and is incorrectly set as an integer (6353) instead of the expected float (6353.45). Could you please look into this issue?

Discussion:

Patch:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index cca18ba0..f5633e42 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -2,20 +2,20 @@ exclude: tests/fixtures
 
 repos:
   - repo: https://github.com/asottile/pyupgrade
-    rev: v3.10.1
+    rev: v3.15.0
     hooks:
       - id: pyupgrade
         args: [ --py38-plus ]
   - repo: https://github.com/asottile/reorder_python_imports
-    rev: v3.10.0
+    rev: v3.12.0
     hooks:
       - id: reorder-python-imports
   - repo: https://github.com/ambv/black
-    rev: 23.7.0
+    rev: 23.11.0
     hooks:
       - id: black
   - repo: https://github.com/PyCQA/autoflake
-    rev: v2.2.0
+    rev: v2.2.1
     hooks:
       - id: autoflake
   - repo: https://github.com/PyCQA/flake8
@@ -30,7 +30,7 @@ repos:
         args: [ "--suppress-none-returning" ]
 
   - repo: https://github.com/pre-commit/pre-commit-hooks
-    rev: v4.4.0
+    rev: v4.5.0
     hooks:
       - id: trailing-whitespace
       - id: end-of-file-fixer
@@ -41,7 +41,7 @@ repos:
       - id: docformatter
         args: [ "--in-place", "--pre-summary-newline" ]
   - repo: https://github.com/pre-commit/mirrors-mypy
-    rev: v1.5.1
+    rev: v1.7.0
     hooks:
       - id: mypy
         files: ^(xsdata/)
diff --git a/xsdata/formats/dataclass/parsers/json.py b/xsdata/formats/dataclass/parsers/json.py
index f163c367..1ea771c0 100644
--- a/xsdata/formats/dataclass/parsers/json.py
+++ b/xsdata/formats/dataclass/parsers/json.py
@@ -16,6 +16,7 @@ from xsdata.exceptions import ConverterWarning
 from xsdata.exceptions import ParserError
 from xsdata.formats.bindings import AbstractParser
 from xsdata.formats.bindings import T
+from xsdata.formats.converter import converter
 from xsdata.formats.dataclass.context import XmlContext
 from xsdata.formats.dataclass.models.elements import XmlMeta
 from xsdata.formats.dataclass.models.elements import XmlVar
@@ -241,6 +242,8 @@ class JsonParser(AbstractParser):
             # field can support any object return the value as it is
             return value
 
+        value = converter.serialize(value)
+
         # Convert value according to the field types
         return ParserUtils.parse_value(
             value=value,
diff --git a/xsdata/formats/dataclass/serializers/mixins.py b/xsdata/formats/dataclass/serializers/mixins.py
index 89a8fb8e..ad31da6d 100644
--- a/xsdata/formats/dataclass/serializers/mixins.py
+++ b/xsdata/formats/dataclass/serializers/mixins.py
@@ -213,7 +213,7 @@ class XmlWriter:
         :param qname: Tag qualified name
         """
         self.flush_start(True)
-        self.handler.endElementNS(split_qname(qname), None)
+        self.handler.endElementNS(split_qname(qname), "")
 
         if self.tail:
             self.handler.characters(self.tail)
@@ -239,20 +239,22 @@ class XmlWriter:
         :param is_nil: If true add ``xsi:nil="true"`` to the element
             attributes
         """
-        if self.pending_tag:
-            if not is_nil:
-                self.attrs.pop(XSI_NIL, None)
+        if not self.pending_tag:
+            return
 
-            for name in self.attrs.keys():
-                self.add_namespace(name[0])
+        if not is_nil:
+            self.attrs.pop(XSI_NIL, None)
 
-            self.reset_default_namespace()
-            self.start_namespaces()
+        for name in self.attrs.keys():
+            self.add_namespace(name[0])
 
-            self.handler.startElementNS(self.pending_tag, None, self.attrs)
-            self.attrs = {}
-            self.in_tail = False
-            self.pending_tag = None
+        self.reset_default_namespace()
+        self.start_namespaces()
+
+        self.handler.startElementNS(self.pending_tag, "", self.attrs)  # type: ignore
+        self.attrs = {}
+        self.in_tail = False
+        self.pending_tag = None
 
     def start_namespaces(self):
         """


Test patch:
diff --git a/tests/fixtures/compound/models.py b/tests/fixtures/compound/models.py
index 5ba18073..04de69e4 100644
--- a/tests/fixtures/compound/models.py
+++ b/tests/fixtures/compound/models.py
@@ -35,7 +35,7 @@ class Root:
     class Meta:
         name = "root"
 
-    alpha_or_bravo: List[Union[Bravo, Alpha]] = field(
+    alpha_or_bravo: List[Union[Alpha, Bravo]] = field(
         default_factory=list,
         metadata={
             "type": "Elements",
diff --git a/tests/formats/dataclass/parsers/test_json.py b/tests/formats/dataclass/parsers/test_json.py
index 277b1a51..f15c23d4 100644
--- a/tests/formats/dataclass/parsers/test_json.py
+++ b/tests/formats/dataclass/parsers/test_json.py
@@ -1,6 +1,9 @@
 import json
+from dataclasses import asdict
+from dataclasses import make_dataclass
 from typing import List
 from typing import Optional
+from typing import Union
 from xml.etree.ElementTree import QName
 
 from tests import fixtures_dir
@@ -370,6 +373,15 @@ class JsonParserTests(FactoryTestCase):
 
         self.assertEqual("Unable to locate xsi:type `notexists`", str(cm.exception))
 
+    def test_bind_text_with_unions(self):
+        Fixture = make_dataclass("Fixture", [("x", List[Union[int, float, str, bool]])])
+        values = ["foo", 12.2, "12.2", 12, "12", True, "false"]
+        data = json.dumps({"x": values})
+
+        result = self.parser.from_string(data, Fixture)
+        expected = ["foo", 12.2, 12.2, 12, 12, True, False]
+        self.assertEqual({"x": expected}, asdict(result))
+
     def test_find_var(self):
         meta = self.parser.context.build(TypeB)
         xml_vars = meta.get_all_vars()