Skip to content

Commit ab4a750

Browse files
chore: bump pyo3 (#276)
* chore: bump pyo3 * fix: prevent double-escaping of XML attribute values in make_valid_xml_name make_valid_xml_name was calling escape_xml(key) and storing the escaped value. Later, make_attr_string would escape it again, producing output like name="tag&amp;amp;name" instead of name="tag&amp;name". Now make_valid_xml_name operates on the raw key and lets make_attr_string handle the single escaping pass. Amp-Thread-ID: https://ampcode.com/threads/T-019ce197-0f45-7644-a010-590c20704f18 Co-authored-by: Amp <amp@ampcode.com> * fix: use Python str() for float formatting to match Python output Rust's f64::to_string() renders 1.0 as "1" while Python gives "1.0". Since this is a drop-in replacement for the Python dicttoxml, use obj.str() to get Python's native float representation. Amp-Thread-ID: https://ampcode.com/threads/T-019ce197-0f45-7644-a010-590c20704f18 Co-authored-by: Amp <amp@ampcode.com> * fix: propagate iteration errors instead of silently swallowing them The try_iter() path used filter_map(|r| r.ok()) which silently dropped any exceptions raised during iteration. Now uses collect::<PyResult<_>>()? to properly propagate errors to the caller. Amp-Thread-ID: https://ampcode.com/threads/T-019ce197-0f45-7644-a010-590c20704f18 Co-authored-by: Amp <amp@ampcode.com> * fix: validate custom_root is a valid XML element name Previously custom_root was written directly into XML tags without validation, allowing arbitrary strings to produce invalid XML output. Now raises ValueError for invalid root element names. Amp-Thread-ID: https://ampcode.com/threads/T-019ce197-0f45-7644-a010-590c20704f18 Co-authored-by: Amp <amp@ampcode.com> * refactor: collapse duplicate dispatch, single-buffer writes, byte-level escaping Major restructuring of the Rust extension: - Collapsed three identical type-dispatch chains (convert_value, convert_dict, convert_list) into a single write_value() function. Removes ~300 lines of duplicated if/else logic. - All XML writing now appends to a single shared String buffer via write_value/write_dict_contents/write_list_contents instead of allocating intermediate Strings at every recursion level. - Replaced char-by-char escape_xml with byte-scanning push_escaped_text (for text nodes, only escapes &<>) and push_escaped_attr (for attributes, also escapes quotes). Copies clean slices in bulk. - Replaced format!-based wrap_cdata with push_cdata that writes directly to the buffer without intermediate allocations. - Replaced to_lowercase().starts_with("xml") in is_valid_xml_name with eq_ignore_ascii_case to avoid allocating a lowercased copy. - Removed unused std::fmt::Write import, added #[derive(Copy, Clone)] to ConvertConfig, gated helper functions behind cfg(python). - Added tests for push_escaped_text, push_escaped_attr, push_cdata. Amp-Thread-ID: https://ampcode.com/threads/T-019ce197-0f45-7644-a010-590c20704f18 Co-authored-by: Amp <amp@ampcode.com> * Fix: rust clippy errors * fix: test failures * fix: do cargo fmt * chore: update the version for the rust binding * feat: update benchmark with the latest rust version --------- Co-authored-by: Amp <amp@ampcode.com>
1 parent a70b9ef commit ab4a750

5 files changed

Lines changed: 421 additions & 534 deletions

File tree

BENCHMARKS.md

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ Comprehensive performance comparison between all json2xml implementations.
66

77
- **Machine**: Apple Silicon (M-series, aarch64)
88
- **OS**: macOS
9-
- **Date**: January 28, 2026
9+
- **Date**: March 12, 2026
1010

1111
### Implementations Tested
1212

@@ -22,32 +22,32 @@ Comprehensive performance comparison between all json2xml implementations.
2222
| Size | Description | Bytes |
2323
|------|-------------|-------|
2424
| Small | Simple object `{"name": "John", "age": 30, "city": "New York"}` | 47 |
25-
| Medium | 10 generated records with nested structures | ~3,208 |
25+
| Medium | 10 generated records with nested structures | ~3,211 |
2626
| bigexample.json | Real-world patent data | 2,018 |
27-
| Large | 100 generated records with nested structures | ~32,205 |
28-
| Very Large | 1,000 generated records with nested structures | ~323,119 |
27+
| Large | 100 generated records with nested structures | ~32,220 |
28+
| Very Large | 1,000 generated records with nested structures | ~323,114 |
2929

3030
## Results
3131

3232
### Performance Summary
3333

3434
| Test Case | Python | Rust | Go | Zig |
3535
|-----------|--------|------|-----|-----|
36-
| Small (47B) | 41.88µs | 1.66µs | 4.52ms | 2.80ms |
37-
| Medium (3.2KB) | 2.19ms | 71.85µs | 4.33ms | 2.18ms |
38-
| bigexample (2KB) | 854.38µs | 30.89µs | 4.28ms | 2.12ms |
39-
| Large (32KB) | 21.57ms | 672.96µs | 4.47ms | 2.48ms |
40-
| Very Large (323KB) | 216.52ms | 6.15ms | 4.44ms | 5.54ms |
36+
| Small (47B) | 78.39µs | 1.05µs | 4.31ms | 1.96ms |
37+
| Medium (3.2KB) | 2.15ms | 15.47µs | 5.03ms | 2.34ms |
38+
| bigexample (2KB) | 862.12µs | 6.44µs | 4.47ms | 2.38ms |
39+
| Large (32KB) | 22.08ms | 150.91µs | 4.80ms | 2.89ms |
40+
| Very Large (323KB) | 218.63ms | 1.47ms | 4.75ms | 5.38ms |
4141

4242
### Speedup vs Pure Python
4343

4444
| Test Case | Rust | Go | Zig |
4545
|-----------|------|-----|-----|
46-
| Small (47B) | **25.2x** | 0.0x* | 0.0x* |
47-
| Medium (3.2KB) | **30.5x** | 0.5x* | 1.0x* |
48-
| bigexample (2KB) | **27.7x** | 0.2x* | 0.4x* |
49-
| Large (32KB) | **32.1x** | 4.8x | **8.7x** |
50-
| Very Large (323KB) | **35.2x** | **48.8x** | **39.1x** |
46+
| Small (47B) | **74.9x** | 0.0x* | 0.0x* |
47+
| Medium (3.2KB) | **139.1x** | 0.4x* | 0.9x* |
48+
| bigexample (2KB) | **133.9x** | 0.2x* | 0.4x* |
49+
| Large (32KB) | **146.3x** | 4.6x | **7.6x** |
50+
| Very Large (323KB) | **149.2x** | **46.1x** | **40.6x** |
5151

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

@@ -56,23 +56,23 @@ Comprehensive performance comparison between all json2xml implementations.
5656
### 1. Rust Extension is the Best Choice for Python Users 🦀
5757

5858
The Rust extension (json2xml-rs) provides:
59-
- **~25-35x faster** than pure Python consistently across all input sizes
59+
- **~75-149x faster** than pure Python consistently across all input sizes
6060
- **Zero process overhead** - called directly from Python
6161
- **Automatic fallback** - pure Python used if Rust unavailable
6262
- **Easy install**: `pip install json2xml[fast]`
6363

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

6666
For very large inputs (323KB+):
67-
- **48.8x faster** than Python
67+
- **46.1x faster** than Python
6868
- But ~4ms startup overhead hurts small file performance
6969
- Best for batch processing or large file conversions
7070

7171
### 3. Zig is Now Highly Competitive ⚡
7272

7373
After recent optimizations:
74-
- **39.1x faster** than Python for very large files
75-
- **8.7x faster** for large files (32KB)
74+
- **40.6x faster** than Python for very large files
75+
- **7.6x faster** for large files (32KB)
7676
- Faster startup than Go (~2ms vs ~4ms)
7777
- Best balance of startup time and throughput
7878

@@ -89,7 +89,7 @@ CLI tools (Go, Zig) have process spawn overhead:
8989

9090
| Use Case | Recommended | Why |
9191
|----------|-------------|-----|
92-
| Python library calls | **Rust** (`pip install json2xml[fast]`) | 25-35x faster, no overhead |
92+
| Python library calls | **Rust** (`pip install json2xml[fast]`) | 75-149x faster, no overhead |
9393
| Small files via CLI | **Zig** (json2xml-zig) | Fastest startup (~2ms) |
9494
| Large files via CLI | **Go** or **Zig** | Both excellent (Go slightly faster) |
9595
| Batch processing | **Go** or **Rust** | Both excellent |

README.rst

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ Installation
4343
4444
pip install json2xml
4545
46-
**With Native Rust Acceleration (28x faster)**
46+
**With Native Rust Acceleration (up to 149x faster)**
4747

4848
For maximum performance, install the optional Rust extension:
4949

@@ -55,7 +55,7 @@ For maximum performance, install the optional Rust extension:
5555
# Or install the Rust extension separately
5656
pip install json2xml-rs
5757
58-
The Rust extension provides **28x faster** conversion compared to pure Python. It's automatically used when available, with seamless fallback to pure Python.
58+
The Rust extension provides **75-149x faster** conversion compared to pure Python. It's automatically used when available, with seamless fallback to pure Python.
5959

6060
**As a CLI Tool**
6161

@@ -301,7 +301,7 @@ Using tools directly:
301301
302302
**Rust Extension Development**
303303

304-
The optional Rust extension (``json2xml-rs``) provides 29x faster performance. To develop or build the Rust extension:
304+
The optional Rust extension (``json2xml-rs``) provides up to 149x faster performance. To develop or build the Rust extension:
305305

306306
Prerequisites:
307307

@@ -428,21 +428,21 @@ For users who need maximum performance within Python, json2xml includes an optio
428428
- Rust Extension
429429
- Speedup
430430
* - **Small JSON** (47 bytes)
431-
- 40µs
432-
- 1.5µs
433-
- **27x**
431+
- 78µs
432+
- 1.05µs
433+
- **75x**
434434
* - **Medium JSON** (3.2 KB)
435-
- 2.1ms
436-
- 71µs
437-
- **30x**
435+
- 2.15ms
436+
- 15µs
437+
- **139x**
438438
* - **Large JSON** (32 KB)
439-
- 21ms
440-
- 740µs
441-
- **28x**
439+
- 22ms
440+
- 151µs
441+
- **146x**
442442
* - **Very Large JSON** (323 KB)
443-
- 213ms
444-
- 7.5ms
445-
- **28x**
443+
- 219ms
444+
- 1.47ms
445+
- **149x**
446446

447447
**Usage with Rust Extension:**
448448

rust/Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "json2xml_rs"
3-
version = "0.1.0"
3+
version = "0.2.0"
44
edition = "2021"
55
description = "Fast native JSON to XML conversion for Python"
66
license = "Apache-2.0"
@@ -14,7 +14,7 @@ default = ["python"]
1414
python = ["pyo3/extension-module", "dep:pyo3"]
1515

1616
[dependencies]
17-
pyo3 = { version = "0.27", optional = true }
17+
pyo3 = { version = "0.28.2", optional = true }
1818

1919
[profile.release]
2020
lto = true

rust/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "maturin"
44

55
[project]
66
name = "json2xml_rs"
7-
version = "0.1.0"
7+
version = "0.2.0"
88
description = "Fast native JSON to XML conversion - Rust extension for json2xml"
99
readme = "README.md"
1010
requires-python = ">=3.9"

0 commit comments

Comments
 (0)