Skip to content

Conversation

@vinitkumar
Copy link
Owner

@vinitkumar vinitkumar commented Nov 28, 2025

Summary by Sourcery

Add optional XPath 3.1 json-to-xml output mode and wire it through the Json2xml interface.

New Features:

  • Introduce XPath 3.1 json-to-xml conversion helpers to generate type-based XML elements and key attributes.
  • Add xpath_format flag to dicttoxml and Json2xml to emit W3C-compliant XPath 3.1 JSON XML with the xpath-functions namespace.

Tests:

  • Add test coverage for XPath 3.1 output across basic, nested, array, null, mixed, complex, escaping, pretty-print, and root-array scenarios.

@sourcery-ai
Copy link
Contributor

sourcery-ai bot commented Nov 28, 2025

Reviewer's Guide

Adds an optional XPath 3.1 json-to-xml output mode to json2xml, including a type-aware converter, wiring through the Json2xml API, and comprehensive tests for the new format.

Sequence diagram for Json2xml to_xml with xpath_format enabled

sequenceDiagram
    actor Developer
    participant Json2xml
    participant dicttoxml
    participant XPath31Converter

    Developer->>Json2xml: __init__(data, wrapper, root, pretty, attr_type, item_wrap, xpath_format=True)
    Developer->>Json2xml: to_xml()
    Json2xml->>dicttoxml: dicttoxml(obj=data, custom_root=wrapper, attr_type=attr_type, item_wrap=item_wrap, xpath_format=True)
    dicttoxml->>XPath31Converter: convert_to_xpath31(obj)
    XPath31Converter-->>dicttoxml: xml_content (map/array/string/number/boolean/null)
    dicttoxml->>dicttoxml: wrap root with xmlns=http://www.w3.org/2005/xpath-functions
    dicttoxml-->>Json2xml: xml_bytes
    Json2xml-->>Developer: xml_bytes or pretty printed XML
Loading

Class diagram for updated Json2xml API and XPath 3.1 helpers

classDiagram
    class Json2xml {
        +dict~str, Any~ data
        +list~Any~ data
        +str wrapper
        +bool root
        +bool pretty
        +bool attr_type
        +bool item_wrap
        +bool xpath_format
        +Json2xml(data, wrapper, root, pretty, attr_type, item_wrap, xpath_format)
        +to_xml() Any
    }

    class dicttoxml {
        +dicttoxml(obj, custom_root, ids, attr_type, item_wrap, item_func, cdata, xml_namespaces, list_headers, xpath_format) bytes
    }

    class XPath31Converter {
        +get_xpath31_tag_name(val) str
        +convert_to_xpath31(obj, parent_key) str
    }

    Json2xml ..> dicttoxml : uses
    dicttoxml ..> XPath31Converter : uses when xpath_format
Loading

Flow diagram for dicttoxml XPath 3.1 mode selection

flowchart TD
    A["Start dicttoxml(obj, ..., xpath_format)"] --> B{xpath_format is True?}
    B -- Yes --> C["xml_content = convert_to_xpath31(obj)"]
    C --> D{xml_content startswith <map?}
    D -- Yes --> E["output_root = replace first <map with <map xmlns=XPATH_FUNCTIONS_NS"]
    D -- No --> F{xml_content startswith <array?}
    F -- Yes --> G["output_root = replace first <array with <array xmlns=XPATH_FUNCTIONS_NS"]
    F -- No --> H["output_root = <map xmlns=XPATH_FUNCTIONS_NS> + xml_content + </map>"]
    E --> I["xml = XML declaration + output_root"]
    G --> I
    H --> I
    I --> J["return xml encoded as utf-8"]

    B -- No --> K["Proceed with existing dicttoxml logic (namespaces, wrappers, attributes, etc.)"]
    K --> L["return xml encoded as utf-8"]
Loading

File-Level Changes

Change Details Files
Introduce XPath 3.1 json-to-xml conversion helpers and root serialization logic.
  • Define XPATH_FUNCTIONS_NS constant for the XPath functions namespace URI.
  • Add get_xpath31_tag_name helper to map Python values to XPath 3.1 element type names, even though the current implementation directly infers types in the main converter.
  • Implement convert_to_xpath31 to recursively serialize Python primitives, dicts, and sequences to XPath 3.1-compliant XML fragments using type-based element names, key attributes, and proper XML escaping.
  • Add early-return branch in dicttoxml to short-circuit the existing dict-to-XML flow when xpath_format is True, building the XML declaration and wrapping the converted fragment in a map/array root with the XPath namespace if necessary.
json2xml/dicttoxml.py
Expose xpath_format option through the public Json2xml API and support list roots.
  • Extend Json2xml constructor to accept data as either dict or list and a new xpath_format flag with default False.
  • Store xpath_format on the Json2xml instance and forward it into dicttoxml in to_xml so the new conversion path is used when requested.
json2xml/json2xml.py
Add tests validating XPath 3.1 output structure, types, escaping, and pretty-print compatibility.
  • Add tests for basic XPath format with strings, numbers, and booleans including namespace presence.
  • Add tests for nested maps, arrays (including mixed-type arrays), nulls, and complex nested structures to verify element names, key attributes, and ordering.
  • Add tests to ensure special characters are escaped correctly in XPath format output.
  • Add tests that verify xpath_format works with pretty printing and when the root is an array rather than a map.
tests/test_json2xml.py

Possibly linked issues

  • #: PR adds an xpath_format mode implementing the requested XPath 3.1 json-to-xml mapping, namespace, and tag structure.

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@codecov
Copy link

codecov bot commented Nov 28, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 99.69%. Comparing base (e5ab104) to head (b3596f5).
⚠️ Report is 3 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff             @@
##           master     #259      +/-   ##
==========================================
+ Coverage   99.30%   99.69%   +0.39%     
==========================================
  Files           3        3              
  Lines         288      330      +42     
==========================================
+ Hits          286      329      +43     
+ Misses          2        1       -1     
Flag Coverage Δ
unittests 99.69% <100.00%> (+0.39%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey there - I've reviewed your changes - here's some feedback:

  • get_xpath31_tag_name is defined but never used; either integrate it into convert_to_xpath31 (e.g., for tag selection) or remove it to avoid dead code.
  • In convert_to_xpath31, the Sequence check will treat bytes/bytearray as arrays; if that’s not intended for JSON-like data, consider explicitly excluding those types alongside str.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- `get_xpath31_tag_name` is defined but never used; either integrate it into `convert_to_xpath31` (e.g., for tag selection) or remove it to avoid dead code.
- In `convert_to_xpath31`, the `Sequence` check will treat `bytes`/`bytearray` as arrays; if that’s not intended for JSON-like data, consider explicitly excluding those types alongside `str`.

## Individual Comments

### Comment 1
<location> `json2xml/dicttoxml.py:236-245` </location>
<code_context>
+    Returns:
+        str: XML string in XPath 3.1 format.
+    """
+    key_attr = f' key="{escape_xml(parent_key)}"' if parent_key is not None else ""
+
+    if obj is None:
+        return f"<null{key_attr}/>"
+
+    if isinstance(obj, bool):
+        return f"<boolean{key_attr}>{str(obj).lower()}</boolean>"
+
+    if isinstance(obj, (int, float, numbers.Number)):
+        return f"<number{key_attr}>{obj}</number>"
+
+    if isinstance(obj, str):
+        return f"<string{key_attr}>{escape_xml(obj)}</string>"
+
+    if isinstance(obj, dict):
+        children = "".join(convert_to_xpath31(v, k) for k, v in obj.items())
+        return f"<map{key_attr}>{children}</map>"
+
</code_context>

<issue_to_address>
**issue:** Non-string dict keys will raise when building the key attribute instead of being handled gracefully.

In `convert_to_xpath31`, `parent_key` is passed directly to `escape_xml`, which assumes a string. When iterating `for k, v in obj.items()`, non-string keys (ints, enums, tuples, etc.) will cause a `TypeError`. To align with the original `dicttoxml` behavior and keep the function resilient, normalize keys before escaping, e.g.:

```python
key_str = str(parent_key) if parent_key is not None else None
key_attr = f' key="{escape_xml(key_str)}"' if key_str is not None else ""
```

This keeps non-string keys supported while still escaping safely.
</issue_to_address>

### Comment 2
<location> `json2xml/json2xml.py:23-26` </location>
<code_context>
     xml_namespaces: dict[str, Any] = {},
-    list_headers: bool = False
+    list_headers: bool = False,
+    xpath_format: bool = False,
 ) -> bytes:
     """
</code_context>

<issue_to_address>
**suggestion:** Json2xml wrapper/root options become ineffective when `xpath_format` is enabled, which could confuse users of this class.

With `xpath_format=True`, `Json2xml.to_xml()` still exposes `wrapper` and `root`, but `dicttoxml`’s XPath mode hardcodes the root element/namespace, so those options have no effect. Please either document that `wrapper`/`root` are ignored when `xpath_format` is enabled, or prevent incompatible combinations (e.g., via validation in `Json2xml`’s constructor).

Suggested implementation:

```python
    def __init__(
        self,
        data: dict[str, Any] | list[Any] | None = None,
        wrapper: str = "all",
        root: bool = True,
        pretty: bool = True,
        attr_type: bool = True,
        item_wrap: bool = True,
        xpath_format: bool = False,

```

```python
        if xpath_format and (wrapper != "all" or not root):
            raise ValueError(
                "Invalid argument combination: when 'xpath_format=True', the "
                "'wrapper' and 'root' options are ignored by dicttoxml. "
                "Use wrapper='all' and root=True with xpath_format=True, or "
                "disable xpath_format if you need custom wrapper/root."
            )

        self.data = data

```

Because I only see part of the file, you may need to adjust the second SEARCH block so that it matches the actual first executable line in `__init__`. The key is:
1. Place the `if xpath_format ...` block at the very beginning of the constructor body, before any assignments like `self.data = data`.
2. Ensure the import section already includes `Any` and `list`/`dict` typing (it likely does given your snippet).
</issue_to_address>

### Comment 3
<location> `tests/test_json2xml.py:232-240` </location>
<code_context>
         if xmldata:
             assert b'encoding="UTF-8"' in xmldata
+
+    def test_xpath_format_basic(self) -> None:
+        """Test XPath 3.1 json-to-xml format with basic types."""
+        data = {"name": "John", "age": 30, "active": True}
+        xmldata = json2xml.Json2xml(data, xpath_format=True, pretty=False).to_xml()
+        if xmldata:
+            assert b'xmlns="http://www.w3.org/2005/xpath-functions"' in xmldata
+            assert b'<string key="name">John</string>' in xmldata
+            assert b'<number key="age">30</number>' in xmldata
+            assert b'<boolean key="active">true</boolean>' in xmldata
+
+    def test_xpath_format_nested_dict(self) -> None:
</code_context>

<issue_to_address>
**suggestion (testing):** Consider adding tests for root-level primitive values (string/number/boolean/null) in XPath format

Current tests cover objects, nested dicts, arrays, and mixed arrays, but not when the root JSON value is a primitive (e.g. `"foo"`, `123`, `true`, `null`). Please add tests for `data = "foo"`, `data = 123`, `data = True/False`, and `data = None` with `xpath_format=True` to verify the expected `<map xmlns=...>` wrapping (or other defined behavior) and lock in namespace handling for root-level primitives.

Suggested implementation:

```python
    def test_xpath_format_nested_dict(self) -> None:
        """Test XPath 3.1 format with nested dictionaries."""
        data = {"person": {"name": "Alice", "age": 25}}
        xmldata = json2xml.Json2xml(data, xpath_format=True, pretty=False).to_xml()
        if xmldata:
            assert b'<map key="person">' in xmldata
            assert b'<string key="name">Alice</string>' in xmldata
            assert b'<number key="age">25</number>' in xmldata

    def test_xpath_format_root_string(self) -> None:
        """Test XPath 3.1 format with a root-level string value."""
        data = "foo"
        xmldata = json2xml.Json2xml(data, xpath_format=True, pretty=False).to_xml()
        if xmldata:
            # Root node should be a string in the XPath namespace
            assert b'<string xmlns="http://www.w3.org/2005/xpath-functions">foo</string>' in xmldata

    def test_xpath_format_root_number(self) -> None:
        """Test XPath 3.1 format with a root-level number value."""
        data = 123
        xmldata = json2xml.Json2xml(data, xpath_format=True, pretty=False).to_xml()
        if xmldata:
            # Root node should be a number in the XPath namespace
            assert b'<number xmlns="http://www.w3.org/2005/xpath-functions">123</number>' in xmldata

    def test_xpath_format_root_boolean(self) -> None:
        """Test XPath 3.1 format with root-level boolean values."""
        for value, expected in ((True, b"true"), (False, b"false")):
            xmldata = json2xml.Json2xml(value, xpath_format=True, pretty=False).to_xml()
            if xmldata:
                # Root node should be a boolean in the XPath namespace
                assert (
                    b'<boolean xmlns="http://www.w3.org/2005/xpath-functions">' + expected + b'</boolean>'
                    in xmldata
                )

    def test_xpath_format_root_null(self) -> None:
        """Test XPath 3.1 format with a root-level null value."""
        data = None
        xmldata = json2xml.Json2xml(data, xpath_format=True, pretty=False).to_xml()
        if xmldata:
            # Root node should be a null in the XPath namespace (likely self-closing)
            assert b'<null xmlns="http://www.w3.org/2005/xpath-functions"' in xmldata

    def test_xpath_format_array(self) -> None:
        """Test XPath 3.1 format with arrays."""

```

If the actual behavior of `json2xml.Json2xml(..., xpath_format=True)` wraps root-level primitives inside a `<map>` instead of using primitive elements at the root, adjust the assertions accordingly, for example:

- Expect `b'<map xmlns="http://www.w3.org/2005/xpath-functions"' in xmldata`
- Expect child nodes like `b'<string key="value">foo</string>' in xmldata` (or whatever key name is actually used).

Run the test suite once to confirm the exact XML shape and tweak the expected substrings if necessary to match the concrete output (e.g., spacing or self-closing tag format for `<null/>`).
</issue_to_address>

### Comment 4
<location> `tests/test_json2xml.py:261-269` </location>
<code_context>
+            assert b'<number>2</number>' in xmldata
+            assert b'<number>3</number>' in xmldata
+
+    def test_xpath_format_null(self) -> None:
+        """Test XPath 3.1 format with null values."""
+        data = {"value": None}
+        xmldata = json2xml.Json2xml(data, xpath_format=True, pretty=False).to_xml()
+        if xmldata:
+            assert b'<null key="value"/>' in xmldata
+
+    def test_xpath_format_mixed_array(self) -> None:
</code_context>

<issue_to_address>
**suggestion (testing):** Add coverage for empty map/array cases in XPath format

Current tests only cover `null` values; please also add cases for empty `{}` and `[]` with `xpath_format=True`. For example, assert that `{}` and `[]` produce `<map>` and `<array>` elements respectively (including the XPath 3.1 namespace at the root, e.g. `<map xmlns="http://www.w3.org/2005/xpath-functions"/>` and the corresponding `<array .../>` / non-root forms) so these empty-container edge cases are verified.

```suggestion
    def test_xpath_format_null(self) -> None:
        """Test XPath 3.1 format with null values."""
        data = {"value": None}
        xmldata = json2xml.Json2xml(data, xpath_format=True, pretty=False).to_xml()
        if xmldata:
            assert b'<null key="value"/>' in xmldata

    def test_xpath_format_empty_map_root(self) -> None:
        """Test XPath 3.1 format with an empty map as the root value."""
        data: dict = {}
        xmldata = json2xml.Json2xml(data, xpath_format=True, pretty=False).to_xml()
        # Root empty map should be serialized as a bare <map> with the XPath 3.1 namespace
        assert xmldata == b'<map xmlns="http://www.w3.org/2005/xpath-functions"/>'

    def test_xpath_format_empty_array_root(self) -> None:
        """Test XPath 3.1 format with an empty array as the root value."""
        data: list = []
        xmldata = json2xml.Json2xml(data, xpath_format=True, pretty=False).to_xml()
        # Root empty array should be serialized as a bare <array> with the XPath 3.1 namespace
        assert xmldata == b'<array xmlns="http://www.w3.org/2005/xpath-functions"/>'

    def test_xpath_format_empty_map_non_root(self) -> None:
        """Test XPath 3.1 format with an empty map as a non-root value."""
        data = {"empty_map": {}}
        xmldata = json2xml.Json2xml(data, xpath_format=True, pretty=False).to_xml()
        if xmldata:
            # Non-root empty map should be represented as a self-closing <map> with a key attribute
            assert b'<map key="empty_map"/>' in xmldata

    def test_xpath_format_empty_array_non_root(self) -> None:
        """Test XPath 3.1 format with an empty array as a non-root value."""
        data = {"empty_array": []}
        xmldata = json2xml.Json2xml(data, xpath_format=True, pretty=False).to_xml()
        if xmldata:
            # Non-root empty array should be represented as a self-closing <array> with a key attribute
            assert b'<array key="empty_array"/>' in xmldata

    def test_xpath_format_mixed_array(self) -> None:
        """Test XPath 3.1 format with mixed type arrays."""
```
</issue_to_address>

### Comment 5
<location> `tests/test_json2xml.py:279-292` </location>
<code_context>
+            assert b'<boolean>true</boolean>' in xmldata
+            assert b'<null/>' in xmldata
+
+    def test_xpath_format_complex_nested(self) -> None:
+        """Test XPath 3.1 format with complex nested structures."""
+        data = {
+            "content": [
+                {"id": 70805774, "value": "1001", "position": [1004.0, 288.0]},
+            ]
+        }
+        xmldata = json2xml.Json2xml(data, xpath_format=True, pretty=False).to_xml()
+        if xmldata:
+            assert b'<array key="content">' in xmldata
+            assert b'<number key="id">70805774</number>' in xmldata
+            assert b'<string key="value">1001</string>' in xmldata
+            assert b'<array key="position">' in xmldata
+            assert b'<number>1004.0</number>' in xmldata
+
+    def test_xpath_format_escaping(self) -> None:
</code_context>

<issue_to_address>
**suggestion (testing):** It may be helpful to assert on the root element shape to prove there is no extra wrapper in XPath mode

Since this test currently validates only inner fragments, consider also asserting on the top-level element to confirm the XPath root shape. For example, check that the root is `<map>` with the expected namespace and that no legacy wrapper like `<all>` is present. That will verify `xpath_format=True` affects the root correctly, not just nested nodes.

```suggestion
    def test_xpath_format_complex_nested(self) -> None:
        """Test XPath 3.1 format with complex nested structures."""
        data = {
            "content": [
                {"id": 70805774, "value": "1001", "position": [1004.0, 288.0]},
            ]
        }
        xmldata = json2xml.Json2xml(data, xpath_format=True, pretty=False).to_xml()
        if xmldata:
            # Assert on the root element shape for XPath mode: a <map> with the XPath namespace
            # and no legacy wrapper such as <all>.
            assert b'<map xmlns="http://www.w3.org/2005/xpath-functions"' in xmldata
            assert b'<all>' not in xmldata

            # Existing inner-fragment assertions
            assert b'<array key="content">' in xmldata
            assert b'<number key="id">70805774</number>' in xmldata
            assert b'<string key="value">1001</string>' in xmldata
            assert b'<array key="position">' in xmldata
            assert b'<number>1004.0</number>' in xmldata
```
</issue_to_address>

### Comment 6
<location> `tests/test_json2xml.py:302-308` </location>
<code_context>
+            assert b"&lt;script&gt;" in xmldata
+            assert b"&apos;xss&apos;" in xmldata
+
+    def test_xpath_format_with_pretty_print(self) -> None:
+        """Test XPath 3.1 format works with pretty printing."""
+        data = {"name": "Test"}
+        xmldata = json2xml.Json2xml(data, xpath_format=True, pretty=True).to_xml()
+        if xmldata:
+            assert 'xmlns="http://www.w3.org/2005/xpath-functions"' in xmldata
+            assert '<string key="name">Test</string>' in xmldata
+
+    def test_xpath_format_root_array(self) -> None:
</code_context>

<issue_to_address>
**issue (bug_risk):** `xmldata` is likely bytes here, so these assertions should consistently work with bytes or decode to str

Earlier in this file `xmldata` is treated as `bytes` (e.g. `b'xmlns=...' in xmldata`), but here the assertions use `str`. If `Json2xml.to_xml()` returns bytes, these will fail on Python 3 due to `str`/`bytes` mismatch. Please either make these assertions use byte literals (e.g. `b'xmlns="..."'`) or decode `xmldata` first (e.g. `xmldata_str = xmldata.decode("utf-8")`) and assert on that, so the pretty-print behavior is actually tested rather than tripping on a type error.
</issue_to_address>

### Comment 7
<location> `tests/test_json2xml.py:294-300` </location>
<code_context>
+            assert b'<array key="position">' in xmldata
+            assert b'<number>1004.0</number>' in xmldata
+
+    def test_xpath_format_escaping(self) -> None:
+        """Test XPath 3.1 format properly escapes special characters."""
+        data = {"text": "<script>alert('xss')</script>"}
+        xmldata = json2xml.Json2xml(data, xpath_format=True, pretty=False).to_xml()
+        if xmldata:
+            assert b"&lt;script&gt;" in xmldata
+            assert b"&apos;xss&apos;" in xmldata
+
+    def test_xpath_format_with_pretty_print(self) -> None:
</code_context>

<issue_to_address>
**suggestion (testing):** Consider also asserting that unsafe characters are not present unescaped alongside the escaped ones

For example, you could extend this test with negative assertions such as `assert b"<script>" not in xmldata` and `assert b"'xss'" not in xmldata` to verify the raw, unescaped content never appears in the output.

```suggestion
    def test_xpath_format_escaping(self) -> None:
        """Test XPath 3.1 format properly escapes special characters."""
        data = {"text": "<script>alert('xss')</script>"}
        xmldata = json2xml.Json2xml(data, xpath_format=True, pretty=False).to_xml()
        if xmldata:
            # Escaped content should be present
            assert b"&lt;script&gt;" in xmldata
            assert b"&apos;xss&apos;" in xmldata
            # Raw, unescaped content must not appear
            assert b"<script>" not in xmldata
            assert b"'xss'" not in xmldata
```
</issue_to_address>

### Comment 8
<location> `json2xml/dicttoxml.py:196` </location>
<code_context>
+XPATH_FUNCTIONS_NS = "http://www.w3.org/2005/xpath-functions"
+
+
+def get_xpath31_tag_name(val: Any) -> str:
+    """
+    Determine XPath 3.1 tag name by Python type.
</code_context>

<issue_to_address>
**issue (complexity):** Consider removing the unused helper and centralizing XPath 3.1 root/namespace handling so the conversion logic is type-driven instead of string-munging based.

You can simplify this change in two focused spots without altering behavior.

### 1. Remove unused `get_xpath31_tag_name`

`get_xpath31_tag_name` is dead code and duplicates the type-dispatch logic in `convert_to_xpath31`. Removing it reduces cognitive load and keeps a single source of truth for type mapping.

```python
# Remove this entirely – it’s not used and duplicates logic.
def get_xpath31_tag_name(val: Any) -> str:
    ...
```

If you want to keep a helper, instead refactor `convert_to_xpath31` to *use* it, but as-is the simplest fix is to delete it.

---

### 2. Simplify `xpath_format` root/wrapping logic

The current implementation:

```python
xml_content = convert_to_xpath31(obj)
output = [
    '<?xml version="1.0" encoding="UTF-8" ?>',
    xml_content.replace("<map", f'<map xmlns="{XPATH_FUNCTIONS_NS}"', 1)
    if xml_content.startswith("<map")
    else xml_content.replace("<array", f'<array xmlns="{XPATH_FUNCTIONS_NS}"', 1)
    if xml_content.startswith("<array")
    else f'<map xmlns="{XPATH_FUNCTIONS_NS}">{xml_content}</map>',
]
return "".join(output).encode("utf-8")
```

relies on string-prefix checks and replacement, which is brittle and harder to read.

You can push the namespace handling into `convert_to_xpath31` and use explicit type checks at the call site, keeping behavior the same:

```python
def convert_to_xpath31(obj: Any, parent_key: str | None = None, root: bool = False) -> str:
    key_attr = f' key="{escape_xml(parent_key)}"' if parent_key is not None else ""
    ns_attr = f' xmlns="{XPATH_FUNCTIONS_NS}"' if root else ""

    if obj is None:
        return f"<null{key_attr}{ns_attr}/>" if root else f"<null{key_attr}/>"
    if isinstance(obj, bool):
        return f"<boolean{key_attr}{ns_attr}>{str(obj).lower()}</boolean>"
    if isinstance(obj, (int, float, numbers.Number)):
        return f"<number{key_attr}{ns_attr}>{obj}</number>"
    if isinstance(obj, str):
        return f"<string{key_attr}{ns_attr}>{escape_xml(obj)}</string>"
    if isinstance(obj, dict):
        children = "".join(convert_to_xpath31(v, k) for k, v in obj.items())
        return f"<map{key_attr}{ns_attr}>{children}</map>"
    if isinstance(obj, Sequence) and not isinstance(obj, str):
        children = "".join(convert_to_xpath31(item) for item in obj)
        return f"<array{key_attr}{ns_attr}>{children}</array>"
    return f"<string{key_attr}{ns_attr}>{escape_xml(str(obj))}</string>"
```

Then `dicttoxml` becomes a straightforward type-based decision without string munging, while preserving your current behavior (root dict/array gets the namespace on that element; scalar roots get wrapped in a `<map>`):

```python
def dicttoxml(..., xpath_format: bool = False) -> bytes:
    ...
    if xpath_format:
        # Container roots get the namespace on their own element.
        if isinstance(obj, dict) or (isinstance(obj, Sequence) and not isinstance(obj, str)):
            xml_body = convert_to_xpath31(obj, root=True)
        else:
            # Scalars/non-containers: wrap in a map with the namespace, as you do now.
            xml_body = (
                f'<map xmlns="{XPATH_FUNCTIONS_NS}">'
                f'{convert_to_xpath31(obj)}'
                f'</map>'
            )

        return f'<?xml version="1.0" encoding="UTF-8" ?>{xml_body}'.encode("utf-8")

    # existing non-xpath_format logic below...
```

This removes nested conditional expressions on strings, makes the root-handling logic explicit and type-driven, and centralizes namespace handling in one place.
</issue_to_address>

### Comment 9
<location> `json2xml/dicttoxml.py:218-219` </location>
<code_context>
def get_xpath31_tag_name(val: Any) -> str:
    """
    Determine XPath 3.1 tag name by Python type.

    See: https://www.w3.org/TR/xpath-functions-31/#func-json-to-xml

    Args:
        val: The value to get the tag name for.

    Returns:
        str: The XPath 3.1 tag name (map, array, string, number, boolean, null).
    """
    if val is None:
        return "null"
    if isinstance(val, bool):
        return "boolean"
    if isinstance(val, dict):
        return "map"
    if isinstance(val, (int, float, numbers.Number)):
        return "number"
    if isinstance(val, Sequence) and not isinstance(val, str):
        return "array"
    if isinstance(val, str):
        return "string"
    return "string"

</code_context>

<issue_to_address>
**suggestion (code-quality):** We've found these issues:

- Lift code into else after jump in control flow ([`reintroduce-else`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/reintroduce-else/))
- Hoist repeated code outside conditional statement ([`hoist-statement-from-if`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/hoist-statement-from-if/))

```suggestion

```
</issue_to_address>

### Comment 10
<location> `tests/test_json2xml.py:235-236` </location>
<code_context>
    def test_xpath_format_basic(self) -> None:
        """Test XPath 3.1 json-to-xml format with basic types."""
        data = {"name": "John", "age": 30, "active": True}
        xmldata = json2xml.Json2xml(data, xpath_format=True, pretty=False).to_xml()
        if xmldata:
            assert b'xmlns="http://www.w3.org/2005/xpath-functions"' in xmldata
            assert b'<string key="name">John</string>' in xmldata
            assert b'<number key="age">30</number>' in xmldata
            assert b'<boolean key="active">true</boolean>' in xmldata

</code_context>

<issue_to_address>
**suggestion (code-quality):** Use named expression to simplify assignment and conditional ([`use-named-expression`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/use-named-expression/))

```suggestion
        if xmldata := json2xml.Json2xml(
            data, xpath_format=True, pretty=False
        ).to_xml():
```
</issue_to_address>

### Comment 11
<location> `tests/test_json2xml.py:245-246` </location>
<code_context>
    def test_xpath_format_nested_dict(self) -> None:
        """Test XPath 3.1 format with nested dictionaries."""
        data = {"person": {"name": "Alice", "age": 25}}
        xmldata = json2xml.Json2xml(data, xpath_format=True, pretty=False).to_xml()
        if xmldata:
            assert b'<map key="person">' in xmldata
            assert b'<string key="name">Alice</string>' in xmldata
            assert b'<number key="age">25</number>' in xmldata

</code_context>

<issue_to_address>
**suggestion (code-quality):** Use named expression to simplify assignment and conditional ([`use-named-expression`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/use-named-expression/))

```suggestion
        if xmldata := json2xml.Json2xml(
            data, xpath_format=True, pretty=False
        ).to_xml():
```
</issue_to_address>

### Comment 12
<location> `tests/test_json2xml.py:254-255` </location>
<code_context>
    def test_xpath_format_array(self) -> None:
        """Test XPath 3.1 format with arrays."""
        data = {"numbers": [1, 2, 3]}
        xmldata = json2xml.Json2xml(data, xpath_format=True, pretty=False).to_xml()
        if xmldata:
            assert b'<array key="numbers">' in xmldata
            assert b'<number>1</number>' in xmldata
            assert b'<number>2</number>' in xmldata
            assert b'<number>3</number>' in xmldata

</code_context>

<issue_to_address>
**suggestion (code-quality):** Use named expression to simplify assignment and conditional ([`use-named-expression`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/use-named-expression/))

```suggestion
        if xmldata := json2xml.Json2xml(
            data, xpath_format=True, pretty=False
        ).to_xml():
```
</issue_to_address>

### Comment 13
<location> `tests/test_json2xml.py:264-265` </location>
<code_context>
    def test_xpath_format_null(self) -> None:
        """Test XPath 3.1 format with null values."""
        data = {"value": None}
        xmldata = json2xml.Json2xml(data, xpath_format=True, pretty=False).to_xml()
        if xmldata:
            assert b'<null key="value"/>' in xmldata

</code_context>

<issue_to_address>
**suggestion (code-quality):** Use named expression to simplify assignment and conditional ([`use-named-expression`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/use-named-expression/))

```suggestion
        if xmldata := json2xml.Json2xml(
            data, xpath_format=True, pretty=False
        ).to_xml():
```
</issue_to_address>

### Comment 14
<location> `tests/test_json2xml.py:271-272` </location>
<code_context>
    def test_xpath_format_mixed_array(self) -> None:
        """Test XPath 3.1 format with mixed type arrays."""
        data = {"items": ["text", 42, True, None]}
        xmldata = json2xml.Json2xml(data, xpath_format=True, pretty=False).to_xml()
        if xmldata:
            assert b'<array key="items">' in xmldata
            assert b'<string>text</string>' in xmldata
            assert b'<number>42</number>' in xmldata
            assert b'<boolean>true</boolean>' in xmldata
            assert b'<null/>' in xmldata

</code_context>

<issue_to_address>
**suggestion (code-quality):** Use named expression to simplify assignment and conditional ([`use-named-expression`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/use-named-expression/))

```suggestion
        if xmldata := json2xml.Json2xml(
            data, xpath_format=True, pretty=False
        ).to_xml():
```
</issue_to_address>

### Comment 15
<location> `tests/test_json2xml.py:286-287` </location>
<code_context>
    def test_xpath_format_complex_nested(self) -> None:
        """Test XPath 3.1 format with complex nested structures."""
        data = {
            "content": [
                {"id": 70805774, "value": "1001", "position": [1004.0, 288.0]},
            ]
        }
        xmldata = json2xml.Json2xml(data, xpath_format=True, pretty=False).to_xml()
        if xmldata:
            assert b'<array key="content">' in xmldata
            assert b'<number key="id">70805774</number>' in xmldata
            assert b'<string key="value">1001</string>' in xmldata
            assert b'<array key="position">' in xmldata
            assert b'<number>1004.0</number>' in xmldata

</code_context>

<issue_to_address>
**suggestion (code-quality):** Use named expression to simplify assignment and conditional ([`use-named-expression`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/use-named-expression/))

```suggestion
        if xmldata := json2xml.Json2xml(
            data, xpath_format=True, pretty=False
        ).to_xml():
```
</issue_to_address>

### Comment 16
<location> `tests/test_json2xml.py:297-298` </location>
<code_context>
    def test_xpath_format_escaping(self) -> None:
        """Test XPath 3.1 format properly escapes special characters."""
        data = {"text": "<script>alert('xss')</script>"}
        xmldata = json2xml.Json2xml(data, xpath_format=True, pretty=False).to_xml()
        if xmldata:
            assert b"&lt;script&gt;" in xmldata
            assert b"&apos;xss&apos;" in xmldata

</code_context>

<issue_to_address>
**suggestion (code-quality):** Use named expression to simplify assignment and conditional ([`use-named-expression`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/use-named-expression/))

```suggestion
        if xmldata := json2xml.Json2xml(
            data, xpath_format=True, pretty=False
        ).to_xml():
```
</issue_to_address>

### Comment 17
<location> `tests/test_json2xml.py:305-306` </location>
<code_context>
    def test_xpath_format_with_pretty_print(self) -> None:
        """Test XPath 3.1 format works with pretty printing."""
        data = {"name": "Test"}
        xmldata = json2xml.Json2xml(data, xpath_format=True, pretty=True).to_xml()
        if xmldata:
            assert 'xmlns="http://www.w3.org/2005/xpath-functions"' in xmldata
            assert '<string key="name">Test</string>' in xmldata

</code_context>

<issue_to_address>
**suggestion (code-quality):** Use named expression to simplify assignment and conditional ([`use-named-expression`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/use-named-expression/))

```suggestion
        if xmldata := json2xml.Json2xml(
            data, xpath_format=True, pretty=True
        ).to_xml():
```
</issue_to_address>

### Comment 18
<location> `tests/test_json2xml.py:313-314` </location>
<code_context>
    def test_xpath_format_root_array(self) -> None:
        """Test XPath 3.1 format with root-level array."""
        data = [1, 2, 3]
        xmldata = json2xml.Json2xml(data, xpath_format=True, pretty=False).to_xml()
        if xmldata:
            assert b'<array xmlns="http://www.w3.org/2005/xpath-functions">' in xmldata
            assert b'<number>1</number>' in xmldata

</code_context>

<issue_to_address>
**suggestion (code-quality):** Use named expression to simplify assignment and conditional ([`use-named-expression`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/use-named-expression/))

```suggestion
        if xmldata := json2xml.Json2xml(
            data, xpath_format=True, pretty=False
        ).to_xml():
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines 236 to 245
key_attr = f' key="{escape_xml(parent_key)}"' if parent_key is not None else ""

if obj is None:
return f"<null{key_attr}/>"

if isinstance(obj, bool):
return f"<boolean{key_attr}>{str(obj).lower()}</boolean>"

if isinstance(obj, (int, float, numbers.Number)):
return f"<number{key_attr}>{obj}</number>"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue: Non-string dict keys will raise when building the key attribute instead of being handled gracefully.

In convert_to_xpath31, parent_key is passed directly to escape_xml, which assumes a string. When iterating for k, v in obj.items(), non-string keys (ints, enums, tuples, etc.) will cause a TypeError. To align with the original dicttoxml behavior and keep the function resilient, normalize keys before escaping, e.g.:

key_str = str(parent_key) if parent_key is not None else None
key_attr = f' key="{escape_xml(key_str)}"' if key_str is not None else ""

This keeps non-string keys supported while still escaping safely.

Comment on lines +279 to +292
def test_xpath_format_complex_nested(self) -> None:
"""Test XPath 3.1 format with complex nested structures."""
data = {
"content": [
{"id": 70805774, "value": "1001", "position": [1004.0, 288.0]},
]
}
xmldata = json2xml.Json2xml(data, xpath_format=True, pretty=False).to_xml()
if xmldata:
assert b'<array key="content">' in xmldata
assert b'<number key="id">70805774</number>' in xmldata
assert b'<string key="value">1001</string>' in xmldata
assert b'<array key="position">' in xmldata
assert b'<number>1004.0</number>' in xmldata
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): It may be helpful to assert on the root element shape to prove there is no extra wrapper in XPath mode

Since this test currently validates only inner fragments, consider also asserting on the top-level element to confirm the XPath root shape. For example, check that the root is <map> with the expected namespace and that no legacy wrapper like <all> is present. That will verify xpath_format=True affects the root correctly, not just nested nodes.

Suggested change
def test_xpath_format_complex_nested(self) -> None:
"""Test XPath 3.1 format with complex nested structures."""
data = {
"content": [
{"id": 70805774, "value": "1001", "position": [1004.0, 288.0]},
]
}
xmldata = json2xml.Json2xml(data, xpath_format=True, pretty=False).to_xml()
if xmldata:
assert b'<array key="content">' in xmldata
assert b'<number key="id">70805774</number>' in xmldata
assert b'<string key="value">1001</string>' in xmldata
assert b'<array key="position">' in xmldata
assert b'<number>1004.0</number>' in xmldata
def test_xpath_format_complex_nested(self) -> None:
"""Test XPath 3.1 format with complex nested structures."""
data = {
"content": [
{"id": 70805774, "value": "1001", "position": [1004.0, 288.0]},
]
}
xmldata = json2xml.Json2xml(data, xpath_format=True, pretty=False).to_xml()
if xmldata:
# Assert on the root element shape for XPath mode: a <map> with the XPath namespace
# and no legacy wrapper such as <all>.
assert b'<map xmlns="http://www.w3.org/2005/xpath-functions"' in xmldata
assert b'<all>' not in xmldata
# Existing inner-fragment assertions
assert b'<array key="content">' in xmldata
assert b'<number key="id">70805774</number>' in xmldata
assert b'<string key="value">1001</string>' in xmldata
assert b'<array key="position">' in xmldata
assert b'<number>1004.0</number>' in xmldata

Comment on lines +302 to +308
def test_xpath_format_with_pretty_print(self) -> None:
"""Test XPath 3.1 format works with pretty printing."""
data = {"name": "Test"}
xmldata = json2xml.Json2xml(data, xpath_format=True, pretty=True).to_xml()
if xmldata:
assert 'xmlns="http://www.w3.org/2005/xpath-functions"' in xmldata
assert '<string key="name">Test</string>' in xmldata
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): xmldata is likely bytes here, so these assertions should consistently work with bytes or decode to str

Earlier in this file xmldata is treated as bytes (e.g. b'xmlns=...' in xmldata), but here the assertions use str. If Json2xml.to_xml() returns bytes, these will fail on Python 3 due to str/bytes mismatch. Please either make these assertions use byte literals (e.g. b'xmlns="..."') or decode xmldata first (e.g. xmldata_str = xmldata.decode("utf-8")) and assert on that, so the pretty-print behavior is actually tested rather than tripping on a type error.

Comment on lines +294 to +300
def test_xpath_format_escaping(self) -> None:
"""Test XPath 3.1 format properly escapes special characters."""
data = {"text": "<script>alert('xss')</script>"}
xmldata = json2xml.Json2xml(data, xpath_format=True, pretty=False).to_xml()
if xmldata:
assert b"&lt;script&gt;" in xmldata
assert b"&apos;xss&apos;" in xmldata
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): Consider also asserting that unsafe characters are not present unescaped alongside the escaped ones

For example, you could extend this test with negative assertions such as assert b"<script>" not in xmldata and assert b"'xss'" not in xmldata to verify the raw, unescaped content never appears in the output.

Suggested change
def test_xpath_format_escaping(self) -> None:
"""Test XPath 3.1 format properly escapes special characters."""
data = {"text": "<script>alert('xss')</script>"}
xmldata = json2xml.Json2xml(data, xpath_format=True, pretty=False).to_xml()
if xmldata:
assert b"&lt;script&gt;" in xmldata
assert b"&apos;xss&apos;" in xmldata
def test_xpath_format_escaping(self) -> None:
"""Test XPath 3.1 format properly escapes special characters."""
data = {"text": "<script>alert('xss')</script>"}
xmldata = json2xml.Json2xml(data, xpath_format=True, pretty=False).to_xml()
if xmldata:
# Escaped content should be present
assert b"&lt;script&gt;" in xmldata
assert b"&apos;xss&apos;" in xmldata
# Raw, unescaped content must not appear
assert b"<script>" not in xmldata
assert b"'xss'" not in xmldata

Comment on lines +271 to +272
xmldata = json2xml.Json2xml(data, xpath_format=True, pretty=False).to_xml()
if xmldata:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (code-quality): Use named expression to simplify assignment and conditional (use-named-expression)

Suggested change
xmldata = json2xml.Json2xml(data, xpath_format=True, pretty=False).to_xml()
if xmldata:
if xmldata := json2xml.Json2xml(
data, xpath_format=True, pretty=False
).to_xml():

Comment on lines +286 to +287
xmldata = json2xml.Json2xml(data, xpath_format=True, pretty=False).to_xml()
if xmldata:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (code-quality): Use named expression to simplify assignment and conditional (use-named-expression)

Suggested change
xmldata = json2xml.Json2xml(data, xpath_format=True, pretty=False).to_xml()
if xmldata:
if xmldata := json2xml.Json2xml(
data, xpath_format=True, pretty=False
).to_xml():

Comment on lines +297 to +298
xmldata = json2xml.Json2xml(data, xpath_format=True, pretty=False).to_xml()
if xmldata:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (code-quality): Use named expression to simplify assignment and conditional (use-named-expression)

Suggested change
xmldata = json2xml.Json2xml(data, xpath_format=True, pretty=False).to_xml()
if xmldata:
if xmldata := json2xml.Json2xml(
data, xpath_format=True, pretty=False
).to_xml():

Comment on lines +305 to +306
xmldata = json2xml.Json2xml(data, xpath_format=True, pretty=True).to_xml()
if xmldata:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (code-quality): Use named expression to simplify assignment and conditional (use-named-expression)

Suggested change
xmldata = json2xml.Json2xml(data, xpath_format=True, pretty=True).to_xml()
if xmldata:
if xmldata := json2xml.Json2xml(
data, xpath_format=True, pretty=True
).to_xml():

Comment on lines +313 to +314
xmldata = json2xml.Json2xml(data, xpath_format=True, pretty=False).to_xml()
if xmldata:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (code-quality): Use named expression to simplify assignment and conditional (use-named-expression)

Suggested change
xmldata = json2xml.Json2xml(data, xpath_format=True, pretty=False).to_xml()
if xmldata:
if xmldata := json2xml.Json2xml(
data, xpath_format=True, pretty=False
).to_xml():


import numbers
from typing import TYPE_CHECKING
from unittest.mock import MagicMock, patch

Check notice

Code scanning / CodeQL

Unused import Note test

Import of 'MagicMock' is not used.

Copilot Autofix

AI 28 days ago

To fix the flagged issue, we should remove the unused imports of MagicMock and patch from the statement from unittest.mock import MagicMock, patch on line 7. Since neither is used in the code shown, the best course of action is to simply delete this import line altogether. No further edits are required, and removing the line will not impact existing functionality.

Suggested changeset 1
tests/test_missing_coverage.py

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/tests/test_missing_coverage.py b/tests/test_missing_coverage.py
--- a/tests/test_missing_coverage.py
+++ b/tests/test_missing_coverage.py
@@ -4,7 +4,6 @@
 
 import numbers
 from typing import TYPE_CHECKING
-from unittest.mock import MagicMock, patch
 
 import pytest
 
EOF
@@ -4,7 +4,6 @@

import numbers
from typing import TYPE_CHECKING
from unittest.mock import MagicMock, patch

import pytest

Copilot is powered by AI and may make mistakes. Always verify output.
from typing import TYPE_CHECKING
from unittest.mock import MagicMock, patch

import pytest

Check notice

Code scanning / CodeQL

Unused import Note test

Import of 'pytest' is not used.

Copilot Autofix

AI 28 days ago

To fix the problem, we should delete the unused import statement import pytest on line 9 of tests/test_missing_coverage.py. This change reduces unnecessary dependencies and improves readability. All other imports appear to be used in the code shown, so no further action regarding imports is needed. Remove only the line containing import pytest and leave everything else unchanged.


Suggested changeset 1
tests/test_missing_coverage.py

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/tests/test_missing_coverage.py b/tests/test_missing_coverage.py
--- a/tests/test_missing_coverage.py
+++ b/tests/test_missing_coverage.py
@@ -6,7 +6,6 @@
 from typing import TYPE_CHECKING
 from unittest.mock import MagicMock, patch
 
-import pytest
 
 from json2xml.dicttoxml import (
     convert_to_xpath31,
EOF
@@ -6,7 +6,6 @@
from typing import TYPE_CHECKING
from unittest.mock import MagicMock, patch

import pytest

from json2xml.dicttoxml import (
convert_to_xpath31,
Copilot is powered by AI and may make mistakes. Always verify output.
Comment on lines +11 to +18
from json2xml.dicttoxml import (
convert_to_xpath31,
dicttoxml,
get_unique_id,
get_xml_type,
get_xpath31_tag_name,
make_id,
)

Check notice

Code scanning / CodeQL

Unused import Note test

Import of 'make_id' is not used.

Copilot Autofix

AI 28 days ago

To fix the problem, the import of make_id in the import statement on line 17 should be removed. We want to modify only the relevant import statement (on line 11–18), taking care not to accidentally remove the rest of the required imports from json2xml.dicttoxml. The best way to fix this is to delete make_id from the tuple of imported objects, while leaving the other imports (convert_to_xpath31, dicttoxml, get_unique_id, get_xml_type, get_xpath31_tag_name) unchanged. No other code or imports need to be modified.

Suggested changeset 1
tests/test_missing_coverage.py

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/tests/test_missing_coverage.py b/tests/test_missing_coverage.py
--- a/tests/test_missing_coverage.py
+++ b/tests/test_missing_coverage.py
@@ -14,7 +14,6 @@
     get_unique_id,
     get_xml_type,
     get_xpath31_tag_name,
-    make_id,
 )
 
 if TYPE_CHECKING:
EOF
@@ -14,7 +14,6 @@
get_unique_id,
get_xml_type,
get_xpath31_tag_name,
make_id,
)

if TYPE_CHECKING:
Copilot is powered by AI and may make mistakes. Always verify output.
@vinitkumar
Copy link
Owner Author

@sjehuda Can you please check my branch here and see if that solves your requirements? https://github.com/vinitkumar/json2xml/tree/feature/xpath31-format

@vinitkumar vinitkumar linked an issue Dec 1, 2025 that may be closed by this pull request
@sjehuda
Copy link

sjehuda commented Dec 2, 2025 via email

@vinitkumar
Copy link
Owner Author

vinitkumar commented Dec 2, 2025

Hi @sjehuda

# Install from the feature/xpath31-format branch
pip install git+https://github.com/vinitkumar/json2xml.git@feature/xpath31-format

# Install into a virtual environment
python3 -m venv venv
source venv/bin/activate
pip install git+https://github.com/vinitkumar/json2xml.git@feature/xpath31-format

# Install with extras (dev dependencies for testing)
pip install "git+https://github.com/vinitkumar/json2xml.git@feature/xpath31-format#egg=json2xml[dev]"

# Install a specific commit (if you want to pin a version)
pip install git+https://github.com/vinitkumar/json2xml.git@109007efe25cc51e80d62be836ebafa7929357ad

# Install in editable mode (for development)
pip install -e git+https://github.com/vinitkumar/json2xml.git@feature/xpath31-format#egg=json2xml

Then use it immediately:


from json2xml import json2xml

data = {"name": "John", "age": 30}
xml = json2xml.Json2xml(data, xpath_format=True, pretty=True).to_xml()
print(xml)

This would give you an idea how it works. Please take any time you want this week. I will only merge and release once the testing has been done for this and is verified to work.

@sjehuda
Copy link

sjehuda commented Dec 5, 2025

I would want first to try it in accordance with your directives, and then further experiment.

May you advise of this issue?

Traceback (most recent call last):
  File "<python-input-2>", line 1, in <module>
    xml = json2xml.Json2xml(data, xpath_format=True, pretty=True).to_xml()
          ^^^^^^^^^^^^^^^^^
AttributeError: module 'json2xml' has no attribute 'Json2xml'

@vinitkumar
Copy link
Owner Author

I would want first to try it in accordance with your directives, and then further experiment.

May you advise of this issue?

Traceback (most recent call last):
  File "<python-input-2>", line 1, in <module>
    xml = json2xml.Json2xml(data, xpath_format=True, pretty=True).to_xml()
          ^^^^^^^^^^^^^^^^^
AttributeError: module 'json2xml' has no attribute 'Json2xml'

Sorry for late reply @sjehuda.

Here is the updated code you need to run:

from json2xml import json2xml

data = {"name": "John", "age": 30}
xml = json2xml.Json2xml(data, xpath_format=True, pretty=True).to_xml()
print(xml)

This should return something like:

<?xml version="1.0" encoding="UTF-8"?>
<map xmlns="http://www.w3.org/2005/xpath-functions">
        <string key="name">John</string>
        <number key="age">30</number>
</map>

@vinitkumar
Copy link
Owner Author

@sjehuda Are you able to test?

@sjehuda
Copy link

sjehuda commented Dec 8, 2025

Mr. Kumar. Good afternoon.

I beg your pardon for this unnecessary delay.

Yes. This code works, and to me, it appears to work as expected.

As you know, I am a lawyer, not a specialist engineer, so I have asked others to try it as well.

https://biglist.com/lists/lists.mulberrytech.com/xsl-list/archives/202512/maillist.html

Nevertheless, it seems that the produced result is good, and probably better than my attempt.

https://git.xmpp-it.net/sch/Focus/src/branch/main/utilities/json-to-xml.py

As I intend to incorporate that functionality into project Slixfeed, I intend to utilize your package "json2xml", not mine, as it would increase probability of reporting of issues, if there are any.

https://git.xmpp-it.net/sch/Slixfeed

Thank you, very much.

Copilot AI review requested due to automatic review settings December 8, 2025 16:29
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request adds XPath 3.1 json-to-xml support to the json2xml library, allowing users to generate W3C-compliant XPath 3.1 JSON XML output with the xpath-functions namespace.

Summary

  • Adds optional xpath_format parameter to dicttoxml() and Json2xml class
  • Implements XPath 3.1 json-to-xml conversion following W3C specification
  • Updates test dependencies from pinned versions to minimum versions (pytest 7.0.1 → 9.0.1)

Reviewed changes

Copilot reviewed 8 out of 10 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
uv.lock Updated dependency versions with future timestamps indicating potential version issues
.coverage Binary coverage database file that should not be committed
pyproject.toml Version bump to 5.3.1 and test dependency update
json2xml/init.py Version update to 5.3.0 (inconsistent with pyproject.toml)
json2xml/json2xml.py Added xpath_format parameter to Json2xml class
json2xml/dicttoxml.py Core XPath 3.1 implementation with type-based conversion
tests/test_json2xml.py Added comprehensive XPath format tests
tests/test_missing_coverage.py Added tests for edge cases and missing coverage
requirements-dev.in Changed from exact pins to minimum version constraints

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

__author__ = """Vinit Kumar"""
__email__ = "mail@vinitkumar.me"
__version__ = "5.2.1"
__version__ = "5.3.0"
Copy link

Copilot AI Dec 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Version mismatch: The version is set to "5.3.1" in pyproject.toml but "5.3.0" in json2xml/init.py. These should be consistent. Typically, init.py should match pyproject.toml as the single source of truth.

Suggested change
__version__ = "5.3.0"
__version__ = "5.3.1"

Copilot uses AI. Check for mistakes.
[project.optional-dependencies]
test = [
"pytest==7.0.1",
"pytest>=8.4.1",
Copy link

Copilot AI Dec 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test dependency constraint changed from pytest==7.0.1 (exact pin) to pytest>=8.4.1 (minimum version). This is a significant breaking change as pytest 7.0.1 is very old (2022) and may have different behavior than pytest 9.x. This change should be intentional and all tests should be verified to work with the newer version.

Copilot uses AI. Check for mistakes.

import numbers
from typing import TYPE_CHECKING
from unittest.mock import MagicMock, patch
Copy link

Copilot AI Dec 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Import of 'MagicMock' is not used.

Suggested change
from unittest.mock import MagicMock, patch
from unittest.mock import patch

Copilot uses AI. Check for mistakes.
from typing import TYPE_CHECKING
from unittest.mock import MagicMock, patch

import pytest
Copy link

Copilot AI Dec 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Import of 'pytest' is not used.

Suggested change
import pytest

Copilot uses AI. Check for mistakes.
get_unique_id,
get_xml_type,
get_xpath31_tag_name,
make_id,
Copy link

Copilot AI Dec 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Import of 'make_id' is not used.

Suggested change
make_id,

Copilot uses AI. Check for mistakes.
@vinitkumar vinitkumar merged commit 26d53d6 into master Dec 8, 2025
48 checks passed
@vinitkumar
Copy link
Owner Author

vinitkumar commented Dec 8, 2025

@sjehuda Good news. I just merged and released a new version of json2xml v5.3.1 (https://github.com/vinitkumar/json2xml/releases/tag/v5.3.1)

This contains the support for xPath as in docs shared above. You are directly just use json2xml==5.3.1 and use as in example and it should just work.

https://pypi.org/project/json2xml/

https://json2xml.readthedocs.io/en/latest/readme.html#xpath-3-1-compliance-options

Also, will you mind adding the project in the credits of your readme where you want to use it. Totally optional, but just wondering if you would something to promote a project that has been so close to my heart.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Functionality to imitate function json-to-xml of XPath 3.1

3 participants