Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions BENCHMARKS.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,16 @@ Comprehensive performance comparison between all json2xml implementations.
- **Python**: 3.14.4
- **Date**: April 24, 2026

To make new runs comparable, record the same fields for your machine before
publishing results:

```bash
python --version
uname -a
sw_vers 2>/dev/null || true
which json2xml-go json2xml-zig 2>/dev/null || true
```

### Implementations Tested

| Implementation | Type | Notes |
Expand Down Expand Up @@ -114,24 +124,85 @@ go install github.com/vinitkumar/json2xml-go@latest

## Running the Benchmarks

Run benchmarks from a clean checkout with the project installed in an isolated
environment. The exact virtual environment tool does not matter; the commands
below use `uv` because it keeps Python setup reproducible.

### Required Tools

| Tool | Required for | Notes |
|------|--------------|-------|
| Python 3.10+ | Pure Python benchmarks | The published run used Python 3.14.4. |
| Rust toolchain | Rust extension benchmarks | Needed only when building `json2xml_rs` locally. |
| maturin | Rust extension benchmarks | Builds and installs the PyO3 extension. |
| json2xml-go | Go CLI benchmarks | Must be on `PATH` as `json2xml-go`. |
| json2xml-zig | Zig CLI benchmarks | Must be on `PATH` as `json2xml-zig`. |

### Environment Setup

```bash
uv venv
source .venv/bin/activate
uv pip install -e .
```

For Rust benchmarks, install the extension into the same environment:

```bash
uv pip install maturin
cd rust
maturin develop --release
cd ..
```

For native CLI benchmarks, install the external tools and verify that the
commands are visible:

```bash
go install github.com/vinitkumar/json2xml-go@latest
which json2xml-go
which json2xml-zig
```

### Comprehensive Benchmark (All Implementations)

Runs pure Python, Rust if `json2xml_rs` imports successfully, Go if
`json2xml-go` is on `PATH`, and Zig if `json2xml-zig` is on `PATH`.

```bash
python benchmark_all.py
```

### Rust vs Python Only

Runs the library-call benchmark for pure Python and the PyO3 Rust extension.
If the extension is not installed, the script prints a warning and reports
Python-only results for reference.

```bash
python benchmark_rust.py
```

### Multi-Python Version Benchmark

Creates per-interpreter virtual environments under `.benchmark_venvs/` and
compares the hard-coded Python paths in `benchmark_multi_python.py`. Edit
`PYTHON_VERSIONS` in that script or install the listed interpreters before
running it. Set `JSON2XML_GO_CLI=/path/to/json2xml-go` if the Go binary is not
named `json2xml-go` on `PATH`.

```bash
python benchmark_multi_python.py
```

### Interpreting CLI Numbers

The Go and Zig rows measure full process startup plus conversion because
`benchmark_all.py` invokes those tools with `subprocess.run`. That is the right
measurement for shell workflows, but it is not directly comparable to Python or
Rust library calls for tiny inputs. For small JSON payloads, process startup can
dominate the result; for large payloads, conversion throughput matters more.

## Related Projects

- **Go version**: [github.com/vinitkumar/json2xml-go](https://github.com/vinitkumar/json2xml-go)
Expand Down
45 changes: 45 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,51 @@ Install the accelerated path:

pip install json2xml[fast]

Real-world Examples
^^^^^^^^^^^^^^^^^^^

Convert a JSON API response in Python when another system expects XML:

.. code-block:: python

from json2xml import json2xml

api_response = {"user": {"id": 7, "name": "Ada"}, "active": True}
print(json2xml.Json2xml(api_response, pretty=False).to_xml().decode("utf-8"))

Output:

.. code-block:: xml

<?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>

Convert a local JSON export from the shell:

.. code-block:: console

cat > orders.json <<'JSON'
{"orders":[{"id":"A100","total":19.99},{"id":"A101","total":5.5}]}
JSON
json2xml-py --no-pretty --no-type orders.json

Output:

.. code-block:: xml

<?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>

Convert stdin in a shell pipeline:

.. code-block:: console

printf '%s\n' '{"event":"deploy","status":"ok"}' | json2xml-py --no-pretty --no-type -

Output:

.. code-block:: xml

<?xml version="1.0" encoding="UTF-8" ?><all><event>deploy</event><status>ok</status></all>

Performance Snapshot
^^^^^^^^^^^^^^^^^^^^

Expand Down
46 changes: 46 additions & 0 deletions docs/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,52 @@ Here's how to use each method:
print(json2xml.Json2xml(data).to_xml())


Real-world Examples
-------------------

Convert a JSON API response in Python when another service expects XML:

.. code-block:: python

from json2xml import json2xml

api_response = {"user": {"id": 7, "name": "Ada"}, "active": True}
print(json2xml.Json2xml(api_response, pretty=False).to_xml().decode("utf-8"))

Output:

.. code-block:: xml

<?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>

Convert a local JSON export from the shell:

.. code-block:: console

cat > orders.json <<'JSON'
{"orders":[{"id":"A100","total":19.99},{"id":"A101","total":5.5}]}
JSON
json2xml-py --no-pretty --no-type orders.json

Output:

.. code-block:: xml

<?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>

Convert stdin in a shell pipeline:

.. code-block:: console

printf '%s\n' '{"event":"deploy","status":"ok"}' | json2xml-py --no-pretty --no-type -

Output:

.. code-block:: xml

<?xml version="1.0" encoding="UTF-8" ?><all><event>deploy</event><status>ok</status></all>


Constructor Parameters
----------------------

Expand Down
30 changes: 25 additions & 5 deletions json2xml/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@

import argparse
import sys
from pathlib import Path
from typing import NoReturn

from json2xml import __version__
Expand Down Expand Up @@ -263,22 +264,36 @@ def read_input(args: argparse.Namespace) -> JSONValue:
try:
return readfromstring(args.string)
except StringReadError as e:
exit_with_error(f"Error parsing JSON string: {e}")
exit_with_error(
"Error: Invalid JSON in --string input. "
f"Pass a valid JSON object, array, string, number, boolean, or null. ({e})"
)

if args.input_file:
if args.input_file == "-":
# Read from stdin
return read_from_stdin()
if not Path(args.input_file).is_file():
exit_with_error(
f"Error: JSON file not found: {args.input_file}. "
"Check the path or use - to read JSON from stdin."
)
try:
return readfromjson(args.input_file)
except JSONReadError as e:
exit_with_error(f"Error reading JSON file: {e}")
exit_with_error(
f"Error: Could not parse JSON file: {args.input_file}. "
f"Check that the file contains valid JSON. ({e})"
)

# Check if there's data on stdin
if not sys.stdin.isatty():
return read_from_stdin()

exit_with_error("Error: No input provided. Use -h for help.")
exit_with_error(
"Error: No input provided. Pass a JSON file, use - for stdin, "
"or provide --string/--url."
)


def read_from_stdin() -> JSONValue:
Expand All @@ -294,10 +309,15 @@ def read_from_stdin() -> JSONValue:
try:
json_str = sys.stdin.read().strip()
if not json_str:
exit_with_error("Error: Empty input")
exit_with_error(
"Error: Empty stdin. Pipe JSON into stdin or pass a file/--string."
)
return readfromstring(json_str)
except StringReadError as e:
exit_with_error(f"Error parsing JSON from stdin: {e}")
exit_with_error(
"Error: Invalid JSON from stdin. Pipe valid JSON into stdin "
f"or pass a file/--string. ({e})"
)


def write_output(output: str | bytes, output_file: str | None) -> None:
Expand Down
4 changes: 3 additions & 1 deletion lat.md/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,10 @@ The benchmark docs record measured implementation tradeoffs so users can choose

The April 2026 benchmark on Apple Silicon shows the Rust extension as the best option for Python library calls, with 57-129x speedups over pure Python and no process overhead. Go and Zig remain useful for native CLI workflows where startup cost is acceptable.

Reproduction docs require contributors to record machine, OS, Python, and tool availability before comparing results. `benchmark_all.py` mixes library calls and CLI subprocesses intentionally, so its Go and Zig rows include process startup overhead.

## CLI entrypoint

The CLI is a thin adapter that parses options, resolves one input source, and forwards those options into the same converter used by the library API.

[[json2xml/cli.py#create_parser]] defines the user-facing flags. [[json2xml/cli.py#read_input]] enforces the source priority rules, and [[json2xml/cli.py#main]] constructs [[json2xml/json2xml.py#Json2xml]] so command-line use and library use stay aligned.
[[json2xml/cli.py#create_parser]] defines the user-facing flags. [[json2xml/cli.py#read_input]] enforces the source priority rules, and [[json2xml/cli.py#main]] constructs [[json2xml/json2xml.py#Json2xml]] so command-line use and library use stay aligned.
6 changes: 6 additions & 0 deletions lat.md/behavior.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ The input helpers convert files, strings, URLs, and stdin into Python data struc

[[json2xml/utils.py#readfromjson]] wraps file and JSON decoding failures in `JSONReadError`. [[json2xml/utils.py#readfromstring]] accepts unknown caller input so invalid-type tests can call it honestly, then rejects non-string inputs and malformed JSON with `StringReadError`. [[json2xml/utils.py#readfromurl]] performs a bounded GET request and raises `URLReadError` for network, non-200, decoding, and JSON parse failures.

## User examples

The public examples favor realistic API, file, and stdin flows with compact before-and-after output that can be checked against the real converter.

README and docs examples use `pretty=False` for scan-friendly output and avoid hidden fixtures. They cover Python API conversion, local JSON exports, and shell pipelines so users can choose the right entry point quickly.

## Conversion output

Default output includes an XML declaration, wraps content in `all`, pretty prints the document, and annotates elements with their source type unless callers disable those features.
Expand Down
20 changes: 20 additions & 0 deletions lat.md/tests.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,18 @@ These tests verify the concrete reader helpers against realistic source behavior

URL input should read valid JSON over HTTP and wrap status, network, and decoding failures in `URLReadError`.

## CLI failure messages

These tests verify common command-line failures return short messages that name the broken input source and point users at the next valid action.

### No input is actionable

Running the CLI without JSON should fail with a message that tells users to pass a file, stdin, string, or URL instead of only reporting a generic error.

### Invalid file JSON names the source

Malformed JSON read from an existing file should mention that file path so users can distinguish file parsing failures from missing-file, string, stdin, or conversion failures.

## Conversion behavior

These tests pin the XML shapes that matter most for interoperability, especially the modes that intentionally diverge from the default serializer.
Expand Down Expand Up @@ -69,3 +81,11 @@ Keys ending in `@flat` should keep their flattening behavior where supported and
### Rust and Python XML name parity

The Rust accelerator and Python serializer should agree on supported XML name normalization cases so fast-path output does not drift silently.

### Fast wrapper uses Rust for supported options

When the optional Rust callable is available and the selected options are Rust-backed, the fast wrapper should dispatch directly to that callable.

### Special keys force Python fallback

Special dictionary keys such as `@attrs` and `@val` should bypass the Rust callable so the Python serializer can preserve legacy attribute semantics.
Loading
Loading