json2xml 6.0.7__tar.gz → 6.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. {json2xml-6.0.7 → json2xml-6.1.0}/CONTRIBUTING.rst +12 -1
  2. {json2xml-6.0.7 → json2xml-6.1.0}/HISTORY.rst +7 -0
  3. {json2xml-6.0.7 → json2xml-6.1.0}/PKG-INFO +133 -3
  4. {json2xml-6.0.7 → json2xml-6.1.0}/README.rst +132 -2
  5. {json2xml-6.0.7 → json2xml-6.1.0}/docs/usage.rst +46 -0
  6. {json2xml-6.0.7 → json2xml-6.1.0}/json2xml/__init__.py +1 -1
  7. {json2xml-6.0.7 → json2xml-6.1.0}/json2xml/cli.py +25 -5
  8. {json2xml-6.0.7 → json2xml-6.1.0}/json2xml.egg-info/PKG-INFO +133 -3
  9. {json2xml-6.0.7 → json2xml-6.1.0}/json2xml.egg-info/SOURCES.txt +1 -0
  10. {json2xml-6.0.7 → json2xml-6.1.0}/pyproject.toml +1 -1
  11. {json2xml-6.0.7 → json2xml-6.1.0}/setup.cfg +1 -1
  12. {json2xml-6.0.7 → json2xml-6.1.0}/tests/test_cli.py +35 -5
  13. json2xml-6.1.0/tests/test_dicttoxml_fast_fallback.py +98 -0
  14. {json2xml-6.0.7 → json2xml-6.1.0}/tests/test_utils.py +2 -2
  15. {json2xml-6.0.7 → json2xml-6.1.0}/AUTHORS.rst +0 -0
  16. {json2xml-6.0.7 → json2xml-6.1.0}/LICENSE +0 -0
  17. {json2xml-6.0.7 → json2xml-6.1.0}/MANIFEST.in +0 -0
  18. {json2xml-6.0.7 → json2xml-6.1.0}/docs/Makefile +0 -0
  19. {json2xml-6.0.7 → json2xml-6.1.0}/docs/authors.rst +0 -0
  20. {json2xml-6.0.7 → json2xml-6.1.0}/docs/benchmarks.rst +0 -0
  21. {json2xml-6.0.7 → json2xml-6.1.0}/docs/conf.py +0 -0
  22. {json2xml-6.0.7 → json2xml-6.1.0}/docs/contributing.rst +0 -0
  23. {json2xml-6.0.7 → json2xml-6.1.0}/docs/history.rst +0 -0
  24. {json2xml-6.0.7 → json2xml-6.1.0}/docs/index.rst +0 -0
  25. {json2xml-6.0.7 → json2xml-6.1.0}/docs/installation.rst +0 -0
  26. {json2xml-6.0.7 → json2xml-6.1.0}/docs/json2xml.rst +0 -0
  27. {json2xml-6.0.7 → json2xml-6.1.0}/docs/make.bat +0 -0
  28. {json2xml-6.0.7 → json2xml-6.1.0}/docs/modules.rst +0 -0
  29. {json2xml-6.0.7 → json2xml-6.1.0}/docs/readme.rst +0 -0
  30. {json2xml-6.0.7 → json2xml-6.1.0}/json2xml/dicttoxml.py +0 -0
  31. {json2xml-6.0.7 → json2xml-6.1.0}/json2xml/dicttoxml_fast.py +0 -0
  32. {json2xml-6.0.7 → json2xml-6.1.0}/json2xml/json2xml.py +0 -0
  33. {json2xml-6.0.7 → json2xml-6.1.0}/json2xml/types.py +0 -0
  34. {json2xml-6.0.7 → json2xml-6.1.0}/json2xml/utils.py +0 -0
  35. {json2xml-6.0.7 → json2xml-6.1.0}/json2xml.egg-info/dependency_links.txt +0 -0
  36. {json2xml-6.0.7 → json2xml-6.1.0}/json2xml.egg-info/entry_points.txt +0 -0
  37. {json2xml-6.0.7 → json2xml-6.1.0}/json2xml.egg-info/requires.txt +0 -0
  38. {json2xml-6.0.7 → json2xml-6.1.0}/json2xml.egg-info/top_level.txt +0 -0
  39. {json2xml-6.0.7 → json2xml-6.1.0}/requirements.in +0 -0
  40. {json2xml-6.0.7 → json2xml-6.1.0}/setup.py +0 -0
  41. {json2xml-6.0.7 → json2xml-6.1.0}/tests/__init__.py +0 -0
  42. {json2xml-6.0.7 → json2xml-6.1.0}/tests/conftest.py +0 -0
  43. {json2xml-6.0.7 → json2xml-6.1.0}/tests/test_dict2xml.py +0 -0
  44. {json2xml-6.0.7 → json2xml-6.1.0}/tests/test_json2xml.py +0 -0
  45. {json2xml-6.0.7 → json2xml-6.1.0}/tests/test_missing_coverage.py +0 -0
  46. {json2xml-6.0.7 → json2xml-6.1.0}/tests/test_rust_dicttoxml.py +0 -0
@@ -9,6 +9,17 @@ helps, and credit will always be given.
9
9
 
10
10
  You can contribute in many ways:
11
11
 
12
+ Good First Contributions
13
+ ------------------------
14
+
15
+ If you are new to the project, these are useful places to start:
16
+
17
+ * Improve examples for real-world API responses, files, and command-line usage.
18
+ * Add small tests around conversion options such as wrappers, list handling, CDATA, and XPath output.
19
+ * Improve benchmark documentation or add a reproducible benchmark case.
20
+ * Polish CLI behavior, error messages, and help text.
21
+ * Review the roadmap in ``ROADMAP.md`` and open a focused issue before starting larger changes.
22
+
12
23
  Types of Contributions
13
24
  ----------------------
14
25
 
@@ -99,7 +110,7 @@ Ready to contribute? Here's how to set up `json2xml` for local development.
99
110
  Rust Extension Development
100
111
  --------------------------
101
112
 
102
- The ``json2xml-rs`` Rust extension provides ~29x faster performance. If you want to contribute to the Rust extension:
113
+ The ``json2xml-rs`` Rust extension provides ~57-129x faster performance in the current benchmark. If you want to contribute to the Rust extension:
103
114
 
104
115
  **Prerequisites**
105
116
 
@@ -1,4 +1,11 @@
1
1
 
2
+ 6.1.0 / 2026-05-04
3
+ ==================
4
+
5
+ * fix: address open contribution issues (#292)
6
+ * docs: improve README and add public roadmap (#287)
7
+
8
+
2
9
  6.0.7 / 2026-04-26
3
10
  ==================
4
11
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: json2xml
3
- Version: 6.0.7
3
+ Version: 6.1.0
4
4
  Summary: Simple Python Library to convert JSON to XML
5
5
  Author-email: Vinit Kumar <mail@vinitkumar.me>
6
6
  License: Apache Software License 2.0
@@ -54,7 +54,7 @@ json2xml
54
54
  .. image:: https://codecov.io/gh/vinitkumar/json2xml/branch/master/graph/badge.svg?token=Yt2h55eTL2
55
55
  :target: https://codecov.io/gh/vinitkumar/json2xml
56
56
 
57
- json2xml is a Python library that allows you to convert JSON data into XML format. It's simple, efficient, and easy to use.
57
+ json2xml is a Python library and CLI for converting JSON data into XML. It is designed for teams that need predictable XML output, Python-first ergonomics, and a faster native path when conversion speed matters.
58
58
 
59
59
  Documentation: https://json2xml.readthedocs.io.
60
60
 
@@ -62,6 +62,137 @@ The library was initially dependent on the `dict2xml` project, but it has now be
62
62
 
63
63
  **Looking for a Go version?** Check out `json2xml-go <https://github.com/vinitkumar/json2xml-go>`_, a Go port of this library with identical features and a native CLI tool.
64
64
 
65
+ Why json2xml?
66
+ ^^^^^^^^^^^^^
67
+
68
+ Use json2xml when you need:
69
+
70
+ * A Python library for turning dictionaries, strings, files, or API responses into XML
71
+ * A command-line tool for quick JSON-to-XML conversion from scripts and terminals
72
+ * Optional Rust acceleration with automatic fallback to the pure Python implementation
73
+ * XPath 3.1 compatible output when standards-friendly XML is required
74
+ * A small, focused project with tests, docs, benchmarks, and multiple native implementation experiments
75
+
76
+ Quick Start
77
+ ^^^^^^^^^^^
78
+
79
+ Install the Python package:
80
+
81
+ .. code-block:: console
82
+
83
+ pip install json2xml
84
+
85
+ Use it from Python:
86
+
87
+ .. code-block:: python
88
+
89
+ from json2xml import json2xml
90
+ from json2xml.utils import readfromstring
91
+
92
+ data = readfromstring('{"name": "Ada", "language": "Python"}')
93
+ print(json2xml.Json2xml(data).to_xml())
94
+
95
+ Use it from the terminal:
96
+
97
+ .. code-block:: console
98
+
99
+ json2xml-py -s '{"name": "Ada", "language": "Python"}'
100
+
101
+ Install the accelerated path:
102
+
103
+ .. code-block:: console
104
+
105
+ pip install json2xml[fast]
106
+
107
+ Real-world Examples
108
+ ^^^^^^^^^^^^^^^^^^^
109
+
110
+ Convert a JSON API response in Python when another system expects XML:
111
+
112
+ .. code-block:: python
113
+
114
+ from json2xml import json2xml
115
+
116
+ api_response = {"user": {"id": 7, "name": "Ada"}, "active": True}
117
+ print(json2xml.Json2xml(api_response, pretty=False).to_xml().decode("utf-8"))
118
+
119
+ Output:
120
+
121
+ .. code-block:: xml
122
+
123
+ <?xml version="1.0" encoding="UTF-8" ?><all><user type="dict"><id type="int">7</id><name type="str">Ada</name></user><active type="bool">true</active></all>
124
+
125
+ Convert a local JSON export from the shell:
126
+
127
+ .. code-block:: console
128
+
129
+ cat > orders.json <<'JSON'
130
+ {"orders":[{"id":"A100","total":19.99},{"id":"A101","total":5.5}]}
131
+ JSON
132
+ json2xml-py --no-pretty --no-type orders.json
133
+
134
+ Output:
135
+
136
+ .. code-block:: xml
137
+
138
+ <?xml version="1.0" encoding="UTF-8" ?><all><orders><item><id>A100</id><total>19.99</total></item><item><id>A101</id><total>5.5</total></item></orders></all>
139
+
140
+ Convert stdin in a shell pipeline:
141
+
142
+ .. code-block:: console
143
+
144
+ printf '%s\n' '{"event":"deploy","status":"ok"}' | json2xml-py --no-pretty --no-type -
145
+
146
+ Output:
147
+
148
+ .. code-block:: xml
149
+
150
+ <?xml version="1.0" encoding="UTF-8" ?><all><event>deploy</event><status>ok</status></all>
151
+
152
+ Performance Snapshot
153
+ ^^^^^^^^^^^^^^^^^^^^
154
+
155
+ The optional Rust extension is the fastest path for Python callers because it avoids process startup overhead and falls back to pure Python when a feature is not supported natively.
156
+
157
+ .. list-table::
158
+ :header-rows: 1
159
+ :widths: 30 20 20 15
160
+
161
+ * - Test Case
162
+ - Pure Python
163
+ - Rust Extension
164
+ - Speedup
165
+ * - **Small JSON** (47 bytes)
166
+ - 31.49µs
167
+ - 0.55µs
168
+ - **56.8x**
169
+ * - **Medium JSON** (3.2 KB)
170
+ - 1.69ms
171
+ - 16.15µs
172
+ - **105.0x**
173
+ * - **Large JSON** (32 KB)
174
+ - 17.97ms
175
+ - 168.21µs
176
+ - **106.8x**
177
+ * - **Very Large JSON** (323 KB)
178
+ - 183.33ms
179
+ - 1.42ms
180
+ - **129.0x**
181
+
182
+ See `BENCHMARKS.md <https://github.com/vinitkumar/json2xml/blob/master/BENCHMARKS.md>`_ for the full Python, Rust, Go, and Zig comparison.
183
+
184
+ Project Roadmap
185
+ ^^^^^^^^^^^^^^^
186
+
187
+ The next phase of json2xml is focused on making the project easier to adopt, benchmark, and contribute to:
188
+
189
+ * Improve examples for common API, file, and CLI workflows
190
+ * Add clearer contribution paths for docs, CLI polish, and benchmark coverage
191
+ * Keep comparing Python, Rust, Go, and Zig implementations with reproducible benchmarks
192
+ * Continue hardening the Rust extension fallback behavior across platforms
193
+
194
+ See `ROADMAP.md <https://github.com/vinitkumar/json2xml/blob/master/ROADMAP.md>`_ for more detail.
195
+
65
196
 
66
197
 
67
198
  Architecture Diagram
@@ -574,4 +705,3 @@ Help and Support to maintain this project
574
705
  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
575
706
 
576
707
  - You can sponsor my work for this plugin here: https://github.com/sponsors/vinitkumar/
577
-
@@ -18,7 +18,7 @@ json2xml
18
18
  .. image:: https://codecov.io/gh/vinitkumar/json2xml/branch/master/graph/badge.svg?token=Yt2h55eTL2
19
19
  :target: https://codecov.io/gh/vinitkumar/json2xml
20
20
 
21
- json2xml is a Python library that allows you to convert JSON data into XML format. It's simple, efficient, and easy to use.
21
+ json2xml is a Python library and CLI for converting JSON data into XML. It is designed for teams that need predictable XML output, Python-first ergonomics, and a faster native path when conversion speed matters.
22
22
 
23
23
  Documentation: https://json2xml.readthedocs.io.
24
24
 
@@ -26,6 +26,137 @@ The library was initially dependent on the `dict2xml` project, but it has now be
26
26
 
27
27
  **Looking for a Go version?** Check out `json2xml-go <https://github.com/vinitkumar/json2xml-go>`_, a Go port of this library with identical features and a native CLI tool.
28
28
 
29
+ Why json2xml?
30
+ ^^^^^^^^^^^^^
31
+
32
+ Use json2xml when you need:
33
+
34
+ * A Python library for turning dictionaries, strings, files, or API responses into XML
35
+ * A command-line tool for quick JSON-to-XML conversion from scripts and terminals
36
+ * Optional Rust acceleration with automatic fallback to the pure Python implementation
37
+ * XPath 3.1 compatible output when standards-friendly XML is required
38
+ * A small, focused project with tests, docs, benchmarks, and multiple native implementation experiments
39
+
40
+ Quick Start
41
+ ^^^^^^^^^^^
42
+
43
+ Install the Python package:
44
+
45
+ .. code-block:: console
46
+
47
+ pip install json2xml
48
+
49
+ Use it from Python:
50
+
51
+ .. code-block:: python
52
+
53
+ from json2xml import json2xml
54
+ from json2xml.utils import readfromstring
55
+
56
+ data = readfromstring('{"name": "Ada", "language": "Python"}')
57
+ print(json2xml.Json2xml(data).to_xml())
58
+
59
+ Use it from the terminal:
60
+
61
+ .. code-block:: console
62
+
63
+ json2xml-py -s '{"name": "Ada", "language": "Python"}'
64
+
65
+ Install the accelerated path:
66
+
67
+ .. code-block:: console
68
+
69
+ pip install json2xml[fast]
70
+
71
+ Real-world Examples
72
+ ^^^^^^^^^^^^^^^^^^^
73
+
74
+ Convert a JSON API response in Python when another system expects XML:
75
+
76
+ .. code-block:: python
77
+
78
+ from json2xml import json2xml
79
+
80
+ api_response = {"user": {"id": 7, "name": "Ada"}, "active": True}
81
+ print(json2xml.Json2xml(api_response, pretty=False).to_xml().decode("utf-8"))
82
+
83
+ Output:
84
+
85
+ .. code-block:: xml
86
+
87
+ <?xml version="1.0" encoding="UTF-8" ?><all><user type="dict"><id type="int">7</id><name type="str">Ada</name></user><active type="bool">true</active></all>
88
+
89
+ Convert a local JSON export from the shell:
90
+
91
+ .. code-block:: console
92
+
93
+ cat > orders.json <<'JSON'
94
+ {"orders":[{"id":"A100","total":19.99},{"id":"A101","total":5.5}]}
95
+ JSON
96
+ json2xml-py --no-pretty --no-type orders.json
97
+
98
+ Output:
99
+
100
+ .. code-block:: xml
101
+
102
+ <?xml version="1.0" encoding="UTF-8" ?><all><orders><item><id>A100</id><total>19.99</total></item><item><id>A101</id><total>5.5</total></item></orders></all>
103
+
104
+ Convert stdin in a shell pipeline:
105
+
106
+ .. code-block:: console
107
+
108
+ printf '%s\n' '{"event":"deploy","status":"ok"}' | json2xml-py --no-pretty --no-type -
109
+
110
+ Output:
111
+
112
+ .. code-block:: xml
113
+
114
+ <?xml version="1.0" encoding="UTF-8" ?><all><event>deploy</event><status>ok</status></all>
115
+
116
+ Performance Snapshot
117
+ ^^^^^^^^^^^^^^^^^^^^
118
+
119
+ The optional Rust extension is the fastest path for Python callers because it avoids process startup overhead and falls back to pure Python when a feature is not supported natively.
120
+
121
+ .. list-table::
122
+ :header-rows: 1
123
+ :widths: 30 20 20 15
124
+
125
+ * - Test Case
126
+ - Pure Python
127
+ - Rust Extension
128
+ - Speedup
129
+ * - **Small JSON** (47 bytes)
130
+ - 31.49µs
131
+ - 0.55µs
132
+ - **56.8x**
133
+ * - **Medium JSON** (3.2 KB)
134
+ - 1.69ms
135
+ - 16.15µs
136
+ - **105.0x**
137
+ * - **Large JSON** (32 KB)
138
+ - 17.97ms
139
+ - 168.21µs
140
+ - **106.8x**
141
+ * - **Very Large JSON** (323 KB)
142
+ - 183.33ms
143
+ - 1.42ms
144
+ - **129.0x**
145
+
146
+ See `BENCHMARKS.md <https://github.com/vinitkumar/json2xml/blob/master/BENCHMARKS.md>`_ for the full Python, Rust, Go, and Zig comparison.
147
+
148
+ Project Roadmap
149
+ ^^^^^^^^^^^^^^^
150
+
151
+ The next phase of json2xml is focused on making the project easier to adopt, benchmark, and contribute to:
152
+
153
+ * Improve examples for common API, file, and CLI workflows
154
+ * Add clearer contribution paths for docs, CLI polish, and benchmark coverage
155
+ * Keep comparing Python, Rust, Go, and Zig implementations with reproducible benchmarks
156
+ * Continue hardening the Rust extension fallback behavior across platforms
157
+
158
+ See `ROADMAP.md <https://github.com/vinitkumar/json2xml/blob/master/ROADMAP.md>`_ for more detail.
159
+
29
160
 
30
161
 
31
162
  Architecture Diagram
@@ -538,4 +669,3 @@ Help and Support to maintain this project
538
669
  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
539
670
 
540
671
  - You can sponsor my work for this plugin here: https://github.com/sponsors/vinitkumar/
541
-
@@ -33,6 +33,52 @@ Here's how to use each method:
33
33
  print(json2xml.Json2xml(data).to_xml())
34
34
 
35
35
 
36
+ Real-world Examples
37
+ -------------------
38
+
39
+ Convert a JSON API response in Python when another service expects XML:
40
+
41
+ .. code-block:: python
42
+
43
+ from json2xml import json2xml
44
+
45
+ api_response = {"user": {"id": 7, "name": "Ada"}, "active": True}
46
+ print(json2xml.Json2xml(api_response, pretty=False).to_xml().decode("utf-8"))
47
+
48
+ Output:
49
+
50
+ .. code-block:: xml
51
+
52
+ <?xml version="1.0" encoding="UTF-8" ?><all><user type="dict"><id type="int">7</id><name type="str">Ada</name></user><active type="bool">true</active></all>
53
+
54
+ Convert a local JSON export from the shell:
55
+
56
+ .. code-block:: console
57
+
58
+ cat > orders.json <<'JSON'
59
+ {"orders":[{"id":"A100","total":19.99},{"id":"A101","total":5.5}]}
60
+ JSON
61
+ json2xml-py --no-pretty --no-type orders.json
62
+
63
+ Output:
64
+
65
+ .. code-block:: xml
66
+
67
+ <?xml version="1.0" encoding="UTF-8" ?><all><orders><item><id>A100</id><total>19.99</total></item><item><id>A101</id><total>5.5</total></item></orders></all>
68
+
69
+ Convert stdin in a shell pipeline:
70
+
71
+ .. code-block:: console
72
+
73
+ printf '%s\n' '{"event":"deploy","status":"ok"}' | json2xml-py --no-pretty --no-type -
74
+
75
+ Output:
76
+
77
+ .. code-block:: xml
78
+
79
+ <?xml version="1.0" encoding="UTF-8" ?><all><event>deploy</event><status>ok</status></all>
80
+
81
+
36
82
  Constructor Parameters
37
83
  ----------------------
38
84
 
@@ -2,4 +2,4 @@
2
2
 
3
3
  __author__ = """Vinit Kumar"""
4
4
  __email__ = "mail@vinitkumar.me"
5
- __version__ = "6.0.7"
5
+ __version__ = "6.1.0"
@@ -43,6 +43,7 @@ from __future__ import annotations
43
43
 
44
44
  import argparse
45
45
  import sys
46
+ from pathlib import Path
46
47
  from typing import NoReturn
47
48
 
48
49
  from json2xml import __version__
@@ -263,22 +264,36 @@ def read_input(args: argparse.Namespace) -> JSONValue:
263
264
  try:
264
265
  return readfromstring(args.string)
265
266
  except StringReadError as e:
266
- exit_with_error(f"Error parsing JSON string: {e}")
267
+ exit_with_error(
268
+ "Error: Invalid JSON in --string input. "
269
+ f"Pass a valid JSON object, array, string, number, boolean, or null. ({e})"
270
+ )
267
271
 
268
272
  if args.input_file:
269
273
  if args.input_file == "-":
270
274
  # Read from stdin
271
275
  return read_from_stdin()
276
+ if not Path(args.input_file).is_file():
277
+ exit_with_error(
278
+ f"Error: JSON file not found: {args.input_file}. "
279
+ "Check the path or use - to read JSON from stdin."
280
+ )
272
281
  try:
273
282
  return readfromjson(args.input_file)
274
283
  except JSONReadError as e:
275
- exit_with_error(f"Error reading JSON file: {e}")
284
+ exit_with_error(
285
+ f"Error: Could not parse JSON file: {args.input_file}. "
286
+ f"Check that the file contains valid JSON. ({e})"
287
+ )
276
288
 
277
289
  # Check if there's data on stdin
278
290
  if not sys.stdin.isatty():
279
291
  return read_from_stdin()
280
292
 
281
- exit_with_error("Error: No input provided. Use -h for help.")
293
+ exit_with_error(
294
+ "Error: No input provided. Pass a JSON file, use - for stdin, "
295
+ "or provide --string/--url."
296
+ )
282
297
 
283
298
 
284
299
  def read_from_stdin() -> JSONValue:
@@ -294,10 +309,15 @@ def read_from_stdin() -> JSONValue:
294
309
  try:
295
310
  json_str = sys.stdin.read().strip()
296
311
  if not json_str:
297
- exit_with_error("Error: Empty input")
312
+ exit_with_error(
313
+ "Error: Empty stdin. Pipe JSON into stdin or pass a file/--string."
314
+ )
298
315
  return readfromstring(json_str)
299
316
  except StringReadError as e:
300
- exit_with_error(f"Error parsing JSON from stdin: {e}")
317
+ exit_with_error(
318
+ "Error: Invalid JSON from stdin. Pipe valid JSON into stdin "
319
+ f"or pass a file/--string. ({e})"
320
+ )
301
321
 
302
322
 
303
323
  def write_output(output: str | bytes, output_file: str | None) -> None:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: json2xml
3
- Version: 6.0.7
3
+ Version: 6.1.0
4
4
  Summary: Simple Python Library to convert JSON to XML
5
5
  Author-email: Vinit Kumar <mail@vinitkumar.me>
6
6
  License: Apache Software License 2.0
@@ -54,7 +54,7 @@ json2xml
54
54
  .. image:: https://codecov.io/gh/vinitkumar/json2xml/branch/master/graph/badge.svg?token=Yt2h55eTL2
55
55
  :target: https://codecov.io/gh/vinitkumar/json2xml
56
56
 
57
- json2xml is a Python library that allows you to convert JSON data into XML format. It's simple, efficient, and easy to use.
57
+ json2xml is a Python library and CLI for converting JSON data into XML. It is designed for teams that need predictable XML output, Python-first ergonomics, and a faster native path when conversion speed matters.
58
58
 
59
59
  Documentation: https://json2xml.readthedocs.io.
60
60
 
@@ -62,6 +62,137 @@ The library was initially dependent on the `dict2xml` project, but it has now be
62
62
 
63
63
  **Looking for a Go version?** Check out `json2xml-go <https://github.com/vinitkumar/json2xml-go>`_, a Go port of this library with identical features and a native CLI tool.
64
64
 
65
+ Why json2xml?
66
+ ^^^^^^^^^^^^^
67
+
68
+ Use json2xml when you need:
69
+
70
+ * A Python library for turning dictionaries, strings, files, or API responses into XML
71
+ * A command-line tool for quick JSON-to-XML conversion from scripts and terminals
72
+ * Optional Rust acceleration with automatic fallback to the pure Python implementation
73
+ * XPath 3.1 compatible output when standards-friendly XML is required
74
+ * A small, focused project with tests, docs, benchmarks, and multiple native implementation experiments
75
+
76
+ Quick Start
77
+ ^^^^^^^^^^^
78
+
79
+ Install the Python package:
80
+
81
+ .. code-block:: console
82
+
83
+ pip install json2xml
84
+
85
+ Use it from Python:
86
+
87
+ .. code-block:: python
88
+
89
+ from json2xml import json2xml
90
+ from json2xml.utils import readfromstring
91
+
92
+ data = readfromstring('{"name": "Ada", "language": "Python"}')
93
+ print(json2xml.Json2xml(data).to_xml())
94
+
95
+ Use it from the terminal:
96
+
97
+ .. code-block:: console
98
+
99
+ json2xml-py -s '{"name": "Ada", "language": "Python"}'
100
+
101
+ Install the accelerated path:
102
+
103
+ .. code-block:: console
104
+
105
+ pip install json2xml[fast]
106
+
107
+ Real-world Examples
108
+ ^^^^^^^^^^^^^^^^^^^
109
+
110
+ Convert a JSON API response in Python when another system expects XML:
111
+
112
+ .. code-block:: python
113
+
114
+ from json2xml import json2xml
115
+
116
+ api_response = {"user": {"id": 7, "name": "Ada"}, "active": True}
117
+ print(json2xml.Json2xml(api_response, pretty=False).to_xml().decode("utf-8"))
118
+
119
+ Output:
120
+
121
+ .. code-block:: xml
122
+
123
+ <?xml version="1.0" encoding="UTF-8" ?><all><user type="dict"><id type="int">7</id><name type="str">Ada</name></user><active type="bool">true</active></all>
124
+
125
+ Convert a local JSON export from the shell:
126
+
127
+ .. code-block:: console
128
+
129
+ cat > orders.json <<'JSON'
130
+ {"orders":[{"id":"A100","total":19.99},{"id":"A101","total":5.5}]}
131
+ JSON
132
+ json2xml-py --no-pretty --no-type orders.json
133
+
134
+ Output:
135
+
136
+ .. code-block:: xml
137
+
138
+ <?xml version="1.0" encoding="UTF-8" ?><all><orders><item><id>A100</id><total>19.99</total></item><item><id>A101</id><total>5.5</total></item></orders></all>
139
+
140
+ Convert stdin in a shell pipeline:
141
+
142
+ .. code-block:: console
143
+
144
+ printf '%s\n' '{"event":"deploy","status":"ok"}' | json2xml-py --no-pretty --no-type -
145
+
146
+ Output:
147
+
148
+ .. code-block:: xml
149
+
150
+ <?xml version="1.0" encoding="UTF-8" ?><all><event>deploy</event><status>ok</status></all>
151
+
152
+ Performance Snapshot
153
+ ^^^^^^^^^^^^^^^^^^^^
154
+
155
+ The optional Rust extension is the fastest path for Python callers because it avoids process startup overhead and falls back to pure Python when a feature is not supported natively.
156
+
157
+ .. list-table::
158
+ :header-rows: 1
159
+ :widths: 30 20 20 15
160
+
161
+ * - Test Case
162
+ - Pure Python
163
+ - Rust Extension
164
+ - Speedup
165
+ * - **Small JSON** (47 bytes)
166
+ - 31.49µs
167
+ - 0.55µs
168
+ - **56.8x**
169
+ * - **Medium JSON** (3.2 KB)
170
+ - 1.69ms
171
+ - 16.15µs
172
+ - **105.0x**
173
+ * - **Large JSON** (32 KB)
174
+ - 17.97ms
175
+ - 168.21µs
176
+ - **106.8x**
177
+ * - **Very Large JSON** (323 KB)
178
+ - 183.33ms
179
+ - 1.42ms
180
+ - **129.0x**
181
+
182
+ See `BENCHMARKS.md <https://github.com/vinitkumar/json2xml/blob/master/BENCHMARKS.md>`_ for the full Python, Rust, Go, and Zig comparison.
183
+
184
+ Project Roadmap
185
+ ^^^^^^^^^^^^^^^
186
+
187
+ The next phase of json2xml is focused on making the project easier to adopt, benchmark, and contribute to:
188
+
189
+ * Improve examples for common API, file, and CLI workflows
190
+ * Add clearer contribution paths for docs, CLI polish, and benchmark coverage
191
+ * Keep comparing Python, Rust, Go, and Zig implementations with reproducible benchmarks
192
+ * Continue hardening the Rust extension fallback behavior across platforms
193
+
194
+ See `ROADMAP.md <https://github.com/vinitkumar/json2xml/blob/master/ROADMAP.md>`_ for more detail.
195
+
65
196
 
66
197
 
67
198
  Architecture Diagram
@@ -574,4 +705,3 @@ Help and Support to maintain this project
574
705
  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
575
706
 
576
707
  - You can sponsor my work for this plugin here: https://github.com/sponsors/vinitkumar/
577
-
@@ -38,6 +38,7 @@ tests/__init__.py
38
38
  tests/conftest.py
39
39
  tests/test_cli.py
40
40
  tests/test_dict2xml.py
41
+ tests/test_dicttoxml_fast_fallback.py
41
42
  tests/test_json2xml.py
42
43
  tests/test_missing_coverage.py
43
44
  tests/test_rust_dicttoxml.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "json2xml"
7
- version = "6.0.7"
7
+ version = "6.1.0"
8
8
  description = "Simple Python Library to convert JSON to XML"
9
9
  readme = "README.rst"
10
10
  requires-python = ">=3.10"
@@ -1,5 +1,5 @@
1
1
  [bumpversion]
2
- current_version = 6.0.7
2
+ current_version = 6.1.0
3
3
  commit = True
4
4
  tag = True
5
5
 
@@ -187,7 +187,7 @@ class TestCLI:
187
187
  text=True,
188
188
  )
189
189
  assert result.returncode == 1
190
- assert "Error" in result.stderr
190
+ assert "Invalid JSON in --string input" in result.stderr
191
191
 
192
192
  def test_no_input_error(self) -> None:
193
193
  """Test error when no input is provided."""
@@ -201,6 +201,7 @@ class TestCLI:
201
201
  )
202
202
  # Should fail with no input error
203
203
  assert result.returncode == 1
204
+ assert "Empty stdin" in result.stderr
204
205
 
205
206
  def test_list_input(self) -> None:
206
207
  """Test handling of JSON array input."""
@@ -267,7 +268,7 @@ class TestCLI:
267
268
  text=True,
268
269
  )
269
270
  assert result.returncode == 1
270
- assert "Empty input" in result.stderr or "Error" in result.stderr
271
+ assert "Empty stdin" in result.stderr
271
272
 
272
273
  def test_stdin_whitespace_only(self) -> None:
273
274
  """Test error handling when stdin contains only whitespace."""
@@ -303,8 +304,10 @@ class TestCLI:
303
304
  text=True,
304
305
  )
305
306
  assert result.returncode == 1
306
- assert "Error" in result.stderr
307
+ assert "JSON file not found" in result.stderr
308
+ assert "use - to read JSON from stdin" in result.stderr
307
309
 
310
+ # @lat: [[tests#CLI failure messages#Invalid file JSON names the source]]
308
311
  def test_invalid_json_file(self) -> None:
309
312
  """Test error handling for file with invalid JSON content."""
310
313
  with tempfile.TemporaryDirectory() as tmpdir:
@@ -318,7 +321,8 @@ class TestCLI:
318
321
  )
319
322
 
320
323
  assert result.returncode == 1
321
- assert "Error" in result.stderr
324
+ assert "Could not parse JSON file" in result.stderr
325
+ assert str(json_file) in result.stderr
322
326
 
323
327
  def test_output_file_permission_error(self) -> None:
324
328
  """Test error handling when output file cannot be written."""
@@ -553,8 +557,34 @@ class TestCLIUnitTests:
553
557
  assert exc_info.value.code == 1
554
558
 
555
559
  captured = capsys.readouterr()
556
- assert "Error reading JSON file" in captured.err
560
+ assert "JSON file not found" in captured.err
561
+
562
+ def test_read_input_existing_json_file_parse_error(self, capsys: CaptureFixture[str]) -> None:
563
+ """Test read_input keeps parse failures distinct from missing file failures."""
564
+ from json2xml.utils import JSONReadError
565
+
566
+ with tempfile.TemporaryDirectory() as tmpdir:
567
+ json_file = Path(tmpdir) / "invalid.json"
568
+ json_file.write_text("{")
569
+
570
+ with patch("json2xml.cli.readfromjson") as mock_read:
571
+ mock_read.side_effect = JSONReadError("Invalid JSON File")
572
+
573
+ args = MagicMock()
574
+ args.url = None
575
+ args.string = None
576
+ args.input_file = str(json_file)
577
+
578
+ with pytest.raises(SystemExit) as exc_info:
579
+ read_input(args)
580
+
581
+ assert exc_info.value.code == 1
582
+
583
+ captured = capsys.readouterr()
584
+ assert "Could not parse JSON file" in captured.err
585
+ assert str(json_file) in captured.err
557
586
 
587
+ # @lat: [[tests#CLI failure messages#No input is actionable]]
558
588
  def test_read_input_no_input_tty(self, capsys: CaptureFixture[str]) -> None:
559
589
  """Test read_input exits when no input provided and stdin is a tty."""
560
590
  with patch("sys.stdin.isatty", return_value=True):
@@ -0,0 +1,98 @@
1
+ """Tests for optional Rust backend selection in dicttoxml_fast."""
2
+ from __future__ import annotations
3
+
4
+ from typing import Any
5
+ from unittest.mock import Mock
6
+
7
+ import pytest
8
+
9
+ import json2xml.dicttoxml_fast as fast_module
10
+
11
+
12
+ def _force_rust_backend(monkeypatch: pytest.MonkeyPatch) -> Mock:
13
+ """Install a fake Rust backend so tests can exercise selection logic without PyO3."""
14
+ rust_backend = Mock(return_value=b"<rust/>")
15
+ monkeypatch.setattr(fast_module, "_USE_RUST", True)
16
+ monkeypatch.setattr(fast_module, "_rust_dicttoxml", rust_backend)
17
+ return rust_backend
18
+
19
+
20
+ # @lat: [[tests#Conversion behavior#Fast wrapper uses Rust for supported options]]
21
+ def test_fast_wrapper_uses_rust_when_available_for_supported_options(
22
+ monkeypatch: pytest.MonkeyPatch,
23
+ ) -> None:
24
+ """Supported option combinations should go through the Rust callable when present."""
25
+ rust_backend = _force_rust_backend(monkeypatch)
26
+
27
+ result = fast_module.dicttoxml(
28
+ {"name": "Ada"},
29
+ root=False,
30
+ custom_root="person",
31
+ attr_type=False,
32
+ item_wrap=False,
33
+ cdata=True,
34
+ list_headers=True,
35
+ )
36
+
37
+ assert result == b"<rust/>"
38
+ rust_backend.assert_called_once_with(
39
+ {"name": "Ada"},
40
+ root=False,
41
+ custom_root="person",
42
+ attr_type=False,
43
+ item_wrap=False,
44
+ cdata=True,
45
+ list_headers=True,
46
+ )
47
+
48
+
49
+ @pytest.mark.parametrize(
50
+ ("kwargs", "expected"),
51
+ [
52
+ ({"ids": [1]}, b'id="'),
53
+ ({"item_func": lambda parent: "entry"}, b"<entry"),
54
+ ({"xml_namespaces": {"demo": "https://example.com/demo"}}, b'xmlns:demo="https://example.com/demo"'),
55
+ ({"xpath_format": True}, b'xmlns="http://www.w3.org/2005/xpath-functions"'),
56
+ ],
57
+ )
58
+ def test_fast_wrapper_falls_back_to_python_for_unsupported_options(
59
+ monkeypatch: pytest.MonkeyPatch,
60
+ kwargs: dict[str, Any],
61
+ expected: bytes,
62
+ ) -> None:
63
+ """Unsupported Rust options should preserve Python semantics instead of calling Rust."""
64
+ rust_backend = _force_rust_backend(monkeypatch)
65
+
66
+ result = fast_module.dicttoxml({"items": [1, 2]}, **kwargs)
67
+
68
+ assert expected in result
69
+ rust_backend.assert_not_called()
70
+
71
+
72
+ # @lat: [[tests#Conversion behavior#Special keys force Python fallback]]
73
+ def test_fast_wrapper_falls_back_to_python_for_special_keys(
74
+ monkeypatch: pytest.MonkeyPatch,
75
+ ) -> None:
76
+ """Special @attrs/@val keys require Python processing even when Rust is installed."""
77
+ rust_backend = _force_rust_backend(monkeypatch)
78
+
79
+ result = fast_module.dicttoxml({"record": {"@attrs": {"id": "7"}, "@val": "Ada"}})
80
+
81
+ assert b'id="7"' in result
82
+ assert b">Ada</record>" in result
83
+ rust_backend.assert_not_called()
84
+
85
+
86
+ def test_fast_wrapper_falls_back_to_python_when_rust_is_unavailable(
87
+ monkeypatch: pytest.MonkeyPatch,
88
+ ) -> None:
89
+ """Contributors without json2xml_rs should still exercise the pure Python fallback."""
90
+ rust_backend = Mock(return_value=b"<rust/>")
91
+ monkeypatch.setattr(fast_module, "_USE_RUST", False)
92
+ monkeypatch.setattr(fast_module, "_rust_dicttoxml", rust_backend)
93
+
94
+ result = fast_module.dicttoxml({"name": "Ada"})
95
+
96
+ assert b"<name" in result
97
+ assert b">Ada</name>" in result
98
+ rust_backend.assert_not_called()
@@ -31,7 +31,7 @@ if TYPE_CHECKING:
31
31
  class JsonTestHandler(BaseHTTPRequestHandler):
32
32
  """Tiny HTTP handler for exercising the real URL reader."""
33
33
 
34
- responses: ClassVar[dict[str, tuple[int, bytes]]] = {
34
+ json_responses: ClassVar[dict[str, tuple[int, bytes]]] = {
35
35
  "/data.json": (200, b'{"key": "value", "number": 42}'),
36
36
  "/api": (200, b'{"result": "success"}'),
37
37
  "/invalid.json": (200, b"invalid json content"),
@@ -41,7 +41,7 @@ class JsonTestHandler(BaseHTTPRequestHandler):
41
41
 
42
42
  def do_GET(self) -> None:
43
43
  path = self.path.split("?", 1)[0]
44
- status, body = self.responses.get(path, (404, b'{"error": "not found"}'))
44
+ status, body = self.json_responses.get(path, (404, b'{"error": "not found"}'))
45
45
  self.send_response(status)
46
46
  self.send_header("Content-Type", "application/json")
47
47
  self.send_header("Content-Length", str(len(body)))
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes