Repository: vinitkumar/json2xml

Issue:
Boolean types are not converted to their XML equivalents.
**Describe the bug**
When converting a JSON object with boolean type values, `Json2xml` is not converting the values to their XML equivalents. `Json2xml` should be exporting the values in the XML as the lowercase words `true` and `false` respectively. Instead, `Json2xml` is exporting them as Python boolean types using the capitalized words `True` and `False`.

**To Reproduce**
Steps to reproduce the behavior:
1. Given the following JSON object:

```json
{
      "boolean": true,
      "boolean_dict_list": [
        {"boolean_dict": {"boolean": true}},
        {"boolean_dict": {"boolean": false}}
      ],
      "boolean_list": [true, false]
}
```

2. Calling the `Json2xml` conversion like so:

```python
xml = json2xml.Json2xml(sample_json, pretty=True).to_xml()
```

3. Produces the following XML:

```xml
<all>
        <boolean type="bool">True</boolean>
        <boolean_dict_list type="list">
                <item type="dict">
                        <boolean_dict type="dict">
                                <boolean type="bool">True</boolean>
                        </boolean_dict>
                </item>
                <item type="dict">
                        <boolean_dict type="dict">
                                <boolean type="bool">False</boolean>
                        </boolean_dict>
                </item>
        </boolean_dict_list>
        <item type="bool">True</item>
        <item type="bool">False</item>
</all>
```
Notice all the boolean values are capitalized instead of being lowercase like they should be in XML and JSON. There also seems to be a problem with the `boolean_list` array, it is missing its parent tag.

**Expected behavior**

`Json2xml` should produce an XML string that looks like this:

```xml
<all>
        <boolean type="bool">true</boolean>
        <boolean_dict_list type="list">
                <item type="dict">
                        <boolean_dict type="dict">
                                <boolean type="bool">true</boolean>
                        </boolean_dict>
                </item>
                <item type="dict">
                        <boolean_dict type="dict">
                                <boolean type="bool">false</boolean>
                        </boolean_dict>
                </item>
        </boolean_dict_list>
        <boolean_list type="list">
            <item type="bool">true</item>
            <item type="bool">false</item>
        </boolean_list>
</all>
```

**Additional context**

The problem with the capitalized boolean values is because of the following statements in the `json2xml.dicttoxml` module:

```python
def convert(obj, ids, attr_type, item_func, cdata, item_wrap, parent="root"):
    """Routes the elements of an object to the right function to convert them
    based on their data type"""

    LOG.info(f'Inside convert(). obj type is: "{type(obj).__name__}", obj="{str(obj)}"')

    item_name = item_func(parent)

    # Booleans are converted using this function because a Python boolean is a subclass of Number
    if isinstance(obj, (numbers.Number, str)):
        return convert_kv(
            key=item_name, val=obj, attr_type=attr_type, attr={}, cdata=cdata
        )

    if hasattr(obj, "isoformat"):
        return convert_kv(
            key=item_name,
            val=obj.isoformat(),
            attr_type=attr_type,
            attr={},
            cdata=cdata,
        )

    # This is never evaluated because Python booleans are subclasses of Python integers
    if isinstance(obj, bool):
        return convert_bool(item_name, obj, attr_type, cdata)
```

Python booleans are subclasses of integers, so the boolean values are passed to `convert_kv` instead of `convert_bool` because an integer is also a `numbers.Number`. The following statements evaluate to `True` in Python:

```python
# Booleans are integers
isinstance(True, int)

# Booleans are numbers
isinstance(True, numbers.Number)

# Booleans are booleans
isinstance(True, bool)
```

Comments:

Patch:
diff --git a/examples/booleanjson.json b/examples/booleanjson.json
new file mode 100644
index 0000000..a784c7b
--- /dev/null
+++ b/examples/booleanjson.json
@@ -0,0 +1,8 @@
+{
+      "boolean": true,
+      "boolean_dict_list": [
+        {"boolean_dict": {"boolean": true}},
+        {"boolean_dict": {"boolean": false}}
+      ],
+      "boolean_list": [true, false]
+}
diff --git a/json2xml/dicttoxml.py b/json2xml/dicttoxml.py
index d5e3f9f..489d55f 100755
--- a/json2xml/dicttoxml.py
+++ b/json2xml/dicttoxml.py
@@ -139,6 +139,13 @@ def convert(obj, ids, attr_type, item_func, cdata, item_wrap, parent="root"):
 
     item_name = item_func(parent)
 
+    # since bool is also a subtype of number.Number and int, the check for bool
+    # never comes and hence we get wrong value for the xml type bool
+    # here, we just change order and check for bool first, because no other
+    # type other than bool can be true for bool check
+    if isinstance(obj, bool):
+        return convert_bool(item_name, obj, attr_type, cdata)
+
     if isinstance(obj, (numbers.Number, str)):
         return convert_kv(
             key=item_name, val=obj, attr_type=attr_type, attr={}, cdata=cdata
@@ -153,9 +160,6 @@ def convert(obj, ids, attr_type, item_func, cdata, item_wrap, parent="root"):
             cdata=cdata,
         )
 
-    if isinstance(obj, bool):
-        return convert_bool(item_name, obj, attr_type, cdata)
-
     if obj is None:
         return convert_none(item_name, "", attr_type, cdata)
 
@@ -185,7 +189,14 @@ def convert_dict(obj, ids, parent, attr_type, item_func, cdata, item_wrap):
 
         key, attr = make_valid_xml_name(key, attr)
 
-        if isinstance(val, (numbers.Number, str)):
+        # since bool is also a subtype of number.Number and int, the check for bool
+        # never comes and hence we get wrong value for the xml type bool
+        # here, we just change order and check for bool first, because no other
+        # type other than bool can be true for bool check
+        if isinstance(val, bool):
+            addline(convert_bool(key, val, attr_type, attr, cdata))
+
+        elif isinstance(val, (numbers.Number, str)):
             addline(
                 convert_kv(
                     key=key, val=val, attr_type=attr_type, attr=attr, cdata=cdata
@@ -203,9 +214,6 @@ def convert_dict(obj, ids, parent, attr_type, item_func, cdata, item_wrap):
                 )
             )
 
-        elif isinstance(val, bool):
-            addline(convert_bool(key, val, attr_type, attr, cdata))
-
         elif isinstance(val, dict):
             if attr_type:
                 attr["type"] = get_xml_type(val)


Test patch:
diff --git a/tests/test_json2xml.py b/tests/test_json2xml.py
index bbf7ae4..872ee32 100644
--- a/tests/test_json2xml.py
+++ b/tests/test_json2xml.py
@@ -176,3 +176,11 @@ class TestJson2xml(unittest.TestCase):
         with pytest.raises(InvalidDataError) as pytest_wrapped_e:
             json2xml.Json2xml(decoded).to_xml()
         assert pytest_wrapped_e.type == InvalidDataError
+
+    def test_read_boolean_data_from_json(self):
+        """Test correct return for boolean types."""
+        data = readfromjson("examples/booleanjson.json")
+        result = json2xml.Json2xml(data).to_xml()
+        dict_from_xml = xmltodict.parse(result)
+        assert dict_from_xml["all"]["boolean"]["#text"] != 'True'
+        assert dict_from_xml["all"]["boolean"]["#text"] == 'true'