Skip to content
Closed
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
65 changes: 33 additions & 32 deletions BENCHMARKS.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,95 +4,96 @@ Comprehensive performance comparison between all json2xml implementations.

## Test Environment

- **Machine**: Apple Silicon (M-series, aarch64)
- **OS**: macOS
- **Date**: March 12, 2026
- **Machine**: Apple Silicon (arm64)
- **OS**: macOS 26.4.1 (Darwin 25.4.0)
- **Python**: 3.14.4
- **Date**: April 24, 2026

### Implementations Tested

| Implementation | Type | Notes |
|----------------|------|-------|
| Python | Library | Pure Python (json2xml) |
| Rust | Library | Native extension via PyO3 (json2xml-rs) |
| Go | CLI | Standalone binary (json2xml-go v1.0.0) |
| Go | CLI | Standalone binary (json2xml-go) |
| Zig | CLI | Standalone binary (json2xml-zig) |

## Test Data

| Size | Description | Bytes |
|------|-------------|-------|
| Small | Simple object `{"name": "John", "age": 30, "city": "New York"}` | 47 |
| Medium | 10 generated records with nested structures | ~3,211 |
| Medium | 10 generated records with nested structures | 3,212 |
| bigexample.json | Real-world patent data | 2,018 |
| Large | 100 generated records with nested structures | ~32,220 |
| Very Large | 1,000 generated records with nested structures | ~323,114 |
| Large | 100 generated records with nested structures | 32,207 |
| Very Large | 1,000 generated records with nested structures | 323,148 |

## Results

### Performance Summary

| Test Case | Python | Rust | Go | Zig |
|-----------|--------|------|-----|-----|
| Small (47B) | 78.39µs | 1.05µs | 4.31ms | 1.96ms |
| Medium (3.2KB) | 2.15ms | 15.47µs | 5.03ms | 2.34ms |
| bigexample (2KB) | 862.12µs | 6.44µs | 4.47ms | 2.38ms |
| Large (32KB) | 22.08ms | 150.91µs | 4.80ms | 2.89ms |
| Very Large (323KB) | 218.63ms | 1.47ms | 4.75ms | 5.38ms |
| Small (47B) | 31.49µs | 0.55µs | 4.09ms | 2.02ms |
| Medium (3.2KB) | 1.69ms | 16.15µs | 4.07ms | 2.09ms |
| bigexample (2KB) | 819.86µs | 6.44µs | 4.37ms | 2.11ms |
| Large (32KB) | 17.97ms | 168.21µs | 4.10ms | 2.42ms |
| Very Large (323KB) | 183.33ms | 1.42ms | 4.20ms | 5.12ms |

### Speedup vs Pure Python

| Test Case | Rust | Go | Zig |
|-----------|------|-----|-----|
| Small (47B) | **74.9x** | 0.0x* | 0.0x* |
| Medium (3.2KB) | **139.1x** | 0.4x* | 0.9x* |
| bigexample (2KB) | **133.9x** | 0.2x* | 0.4x* |
| Large (32KB) | **146.3x** | 4.6x | **7.6x** |
| Very Large (323KB) | **149.2x** | **46.1x** | **40.6x** |
| Small (47B) | **56.8x** | 0.0x* | 0.0x* |
| Medium (3.2KB) | **105.0x** | 0.4x* | 0.8x* |
| bigexample (2KB) | **127.2x** | 0.2x* | 0.4x* |
| Large (32KB) | **106.8x** | 4.4x | **7.4x** |
| Very Large (323KB) | **129.0x** | **43.6x** | **35.8x** |

*CLI tools have process spawn overhead (~2-4ms) which dominates for small inputs
*CLI tools have process spawn overhead (~2-4ms) which dominates for small inputs.

## Key Observations

### 1. Rust Extension is the Best Choice for Python Users 🦀

The Rust extension (json2xml-rs) provides:
- **~75-149x faster** than pure Python consistently across all input sizes
- **~57-129x faster** conversion than pure Python in this run
- **Zero process overhead** - called directly from Python
- **Automatic fallback** - pure Python used if Rust unavailable
- **Automatic fallback** - pure Python used if Rust is unavailable or a feature requires it
- **Easy install**: `pip install json2xml[fast]`

### 2. Go Excels for Very Large CLI Workloads 🚀

For very large inputs (323KB+):
- **46.1x faster** than Python
- **43.6x faster** than Python
- But ~4ms startup overhead hurts small file performance
- Best for batch processing or large file conversions
- Best for batch processing or large file conversions from shell scripts

### 3. Zig is Now Highly Competitive ⚡
### 3. Zig is Highly Competitive for CLI Use

After recent optimizations:
- **40.6x faster** than Python for very large files
- **7.6x faster** for large files (32KB)
In this run:
- **35.8x faster** than Python for very large files
- **7.4x faster** for large files (32KB)
- Faster startup than Go (~2ms vs ~4ms)
- Best balance of startup time and throughput
- Best balance of startup time and throughput for mixed CLI workloads

### 4. Process Spawn Overhead Matters

CLI tools (Go, Zig) have process spawn overhead:
- Go: ~4ms startup overhead
- Zig: ~2ms startup overhead
- Dominates for small inputs (makes them appear slower than Python!)
- Dominates for small inputs (makes them appear slower than Python)
- Negligible for large inputs where actual work dominates
- Rust extension avoids this entirely by being a native Python module

## When to Use Each Implementation

| Use Case | Recommended | Why |
|----------|-------------|-----|
| Python library calls | **Rust** (`pip install json2xml[fast]`) | 75-149x faster, no overhead |
| Small files via CLI | **Zig** (json2xml-zig) | Fastest startup (~2ms) |
| Large files via CLI | **Go** or **Zig** | Both excellent (Go slightly faster) |
| Batch processing | **Go** or **Rust** | Both excellent |
| Python library calls | **Rust** (`pip install json2xml[fast]`) | 57-129x faster, no process overhead |
| Small files via CLI | **Zig** (json2xml-zig) | Fastest startup among native CLIs (~2ms) |
| Large files via CLI | **Go** or **Zig** | Both excellent; Zig wins at 32KB, Go wins at 323KB in this run |
| Batch processing | **Go** or **Rust** | Both excellent depending on shell vs Python integration |
| Pure Python required | **Python** (json2xml) | Always available |

## Installation
Expand Down
88 changes: 44 additions & 44 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ Installation

pip install json2xml

**With Native Rust Acceleration (up to 149x faster)**
**With Native Rust Acceleration (up to 129x faster)**

For maximum performance, install the optional Rust extension:

Expand All @@ -55,7 +55,7 @@ For maximum performance, install the optional Rust extension:
# Or install the Rust extension separately
pip install json2xml-rs

The Rust extension provides **75-149x faster** conversion compared to pure Python. It's automatically used when available, with seamless fallback to pure Python.
The Rust extension provides **57-129x faster** conversion compared to pure Python in the latest benchmark. It's automatically used when available, with seamless fallback to pure Python.

**As a CLI Tool**

Expand Down Expand Up @@ -301,7 +301,7 @@ Using tools directly:

**Rust Extension Development**

The optional Rust extension (``json2xml-rs``) provides up to 149x faster performance. To develop or build the Rust extension:
The optional Rust extension (``json2xml-rs``) provides up to 129x faster performance in the latest benchmark. To develop or build the Rust extension:

Prerequisites:

Expand Down Expand Up @@ -428,21 +428,21 @@ For users who need maximum performance within Python, json2xml includes an optio
- Rust Extension
- Speedup
* - **Small JSON** (47 bytes)
- 78µs
- 1.05µs
- **75x**
- 31.49µs
- 0.55µs
- **56.8x**
* - **Medium JSON** (3.2 KB)
- 2.15ms
- 15µs
- **139x**
- 1.69ms
- 16.15µs
- **105.0x**
* - **Large JSON** (32 KB)
- 22ms
- 151µs
- **146x**
- 17.97ms
- 168.21µs
- **106.8x**
* - **Very Large JSON** (323 KB)
- 219ms
- 1.47ms
- **149x**
- 183.33ms
- 1.42ms
- **129.0x**

**Usage with Rust Extension:**

Expand Down Expand Up @@ -472,7 +472,7 @@ For other platforms, the pure Python version is used automatically.
Performance Benchmarks
^^^^^^^^^^^^^^^^^^^^^^

Comprehensive benchmarks comparing all implementations (Apple Silicon, January 2026):
Comprehensive benchmarks comparing all implementations (Apple Silicon, macOS 26.4.1, Python 3.14.4, April 2026):

.. list-table::
:header-rows: 1
Expand All @@ -485,40 +485,40 @@ Comprehensive benchmarks comparing all implementations (Apple Silicon, January 2
- Zig
- Best
* - **Small** (47B)
- 40µs
- 1.5µs
- 4.6ms
- 3.7ms
- Rust (28x)
- 31.49µs
- 0.55µs
- 4.09ms
- 2.02ms
- Rust (56.8x)
* - **Medium** (3.2KB)
- 2.1ms
- 71µs
- 4.1ms
- 3.3ms
- Rust (30x)
- 1.69ms
- 16.15µs
- 4.07ms
- 2.09ms
- Rust (105.0x)
* - **Large** (32KB)
- 21ms
- 740µs
- 4ms
- 6.1ms
- Rust (28x)
- 17.97ms
- 168.21µs
- 4.10ms
- 2.42ms
- Rust (106.8x)
* - **Very Large** (323KB)
- 213ms
- 7.5ms
- 4.4ms
- 33ms
- Go (48x)
- 183.33ms
- 1.42ms
- 4.20ms
- 5.12ms
- Rust (129.0x)

**Key Findings:**

- **Rust extension**: ~28x faster than Python, zero overhead (best for Python users)
- **Go CLI**: 48x faster for large files (300KB+), but has ~4ms startup overhead
- **Zig CLI**: 3-6x faster for medium-large files
- **Rust extension**: 57-129x faster than Python, zero process overhead (best for Python users)
- **Go CLI**: 43.6x faster for very large files (300KB+), but has ~4ms startup overhead
- **Zig CLI**: 7.4x faster for large files and 35.8x faster for very large files, with ~2ms startup overhead

**Recommendation by Use Case:**

- **Python library calls**: Use ``pip install json2xml[fast]`` (Rust, 28x faster)
- **Large file CLI processing**: Use `json2xml-go <https://github.com/vinitkumar/json2xml-go>`_ (Go, 48x for 300KB+)
- **Python library calls**: Use ``pip install json2xml[fast]`` (Rust, up to 129x faster)
- **Large file CLI processing**: Use `json2xml-go <https://github.com/vinitkumar/json2xml-go>`_ or `json2xml-zig <https://github.com/vinitkumar/json2xml-zig>`_ depending on your workload
- **Pure Python required**: Use ``pip install json2xml``

For detailed benchmarks, see `BENCHMARKS.md <https://github.com/vinitkumar/json2xml/blob/master/BENCHMARKS.md>`_.
Expand All @@ -529,9 +529,9 @@ Other Implementations

This library is also available in other languages:

- **Rust**: `json2xml-rs <https://pypi.org/project/json2xml-rs/>`_ - 28x faster, Python extension via PyO3
- **Go**: `json2xml-go <https://github.com/vinitkumar/json2xml-go>`_ - 48x faster for large files, native CLI
- **Zig**: `json2xml-zig <https://github.com/vinitkumar/json2xml-zig>`_ - 6x faster, native CLI
- **Rust**: `json2xml-rs <https://pypi.org/project/json2xml-rs/>`_ - up to 129x faster, Python extension via PyO3
- **Go**: `json2xml-go <https://github.com/vinitkumar/json2xml-go>`_ - 43.6x faster for very large files, native CLI
- **Zig**: `json2xml-zig <https://github.com/vinitkumar/json2xml-zig>`_ - 35.8x faster for very large files, native CLI


Help and Support to maintain this project
Expand Down
31 changes: 16 additions & 15 deletions json2xml/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,11 @@

import argparse
import sys
from typing import Any
from typing import NoReturn

from json2xml import __version__
from json2xml.json2xml import Json2xml
from json2xml.types import JSONValue
from json2xml.utils import (
JSONReadError,
StringReadError,
Expand All @@ -60,6 +61,12 @@
EMAIL = "mail@vinitkumar.me"


def exit_with_error(message: str) -> NoReturn:
"""Print an error message and terminate CLI processing."""
print(message, file=sys.stderr)
raise SystemExit(1)


# @lat: [[architecture#CLI entrypoint]]
def create_parser() -> argparse.ArgumentParser:
"""Create and configure the argument parser."""
Expand Down Expand Up @@ -230,7 +237,7 @@


# @lat: [[behavior#Input readers]]
def read_input(args: argparse.Namespace) -> dict[str, Any] | list[Any]:
def read_input(args: argparse.Namespace) -> JSONValue:

Check notice

Code scanning / CodeQL

Explicit returns mixed with implicit (fall through) returns Note

Mixing implicit and explicit returns may indicate an error, as implicit returns always return None.
"""
Read JSON input from the specified source.

Expand All @@ -250,15 +257,13 @@
try:
return readfromurl(args.url)
except URLReadError as e:
print(f"Error reading from URL: {e}", file=sys.stderr)
sys.exit(1)
exit_with_error(f"Error reading from URL: {e}")

if args.string:
try:
return readfromstring(args.string)
except StringReadError as e:
print(f"Error parsing JSON string: {e}", file=sys.stderr)
sys.exit(1)
exit_with_error(f"Error parsing JSON string: {e}")

if args.input_file:
if args.input_file == "-":
Expand All @@ -267,18 +272,16 @@
try:
return readfromjson(args.input_file)
except JSONReadError as e:
print(f"Error reading JSON file: {e}", file=sys.stderr)
sys.exit(1)
exit_with_error(f"Error reading JSON file: {e}")

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

print("Error: No input provided. Use -h for help.", file=sys.stderr)
sys.exit(1)
exit_with_error("Error: No input provided. Use -h for help.")


def read_from_stdin() -> dict[str, Any] | list[Any]:
def read_from_stdin() -> JSONValue:
"""
Read JSON from standard input.

Expand All @@ -291,12 +294,10 @@
try:
json_str = sys.stdin.read().strip()
if not json_str:
print("Error: Empty input", file=sys.stderr)
sys.exit(1)
exit_with_error("Error: Empty input")
return readfromstring(json_str)
except StringReadError as e:
print(f"Error parsing JSON from stdin: {e}", file=sys.stderr)
sys.exit(1)
exit_with_error(f"Error parsing JSON from stdin: {e}")


def write_output(output: str | bytes, output_file: str | None) -> None:
Expand Down
Loading
Loading