Why I Rewrote json2xml in C—After Making It Fast with Rust | Vinit Kumar
The obvious question is: why write json2xml again in C when the Rust version was already fast?
The Rust extension was—and still is—a successful part of the Python json2xml package. It made the common conversion path dramatically faster without changing the Python API. For Python users, that is exactly the right design: install the package, keep the familiar interface, and get native performance where it matters.
But I wanted to explore a different product: a small, dependency-free converter that does not require Python at all. Something that can be installed as a single command-line executable, embedded as a C library, or shipped in environments where adding a language runtime is unnecessary overhead.
That distinction matters. json2xml-c is not an attempt to prove that C is “better” than Rust. It is an experiment in reducing the whole stack: parse JSON, build the value tree, generate XML, and expose the result through a compact C23 API and a standalone CLI. Once that goal was clear, the rewrite made sense.
What the benchmark actually compares
The benchmark compares the complete C conversion path with the Rust-accelerated path used by the Python package. That wording is important.
For C, the timed operation takes JSON bytes already in memory, parses them, and serializes the resulting value tree to XML. For the Python/Rust path, it runs json.loads and then passes the Python object to the Rust-backed serializer. A third measurement times only that serializer with an already parsed Python object.
Before recording any timings, the harness verifies that both implementations produce byte-for-byte identical XML for each fixture. Each result is the median of 21 samples, with enough repetitions to keep every sample running for at least 50 milliseconds. Fixture generation, file reads, and process startup are outside the timed section.
Here are the results from the recorded Apple Silicon run:
Input<br>JSON size<br>C: parse + serialize<br>Python/Rust: parse + serialize<br>Rust-backed serialize only<br>C speedup over full pipeline
Small<br>42 B<br>511 ns<br>2.76 µs<br>1.83 µs<br>5.39x
Medium<br>2,929 B<br>29.44 µs<br>46.79 µs<br>35.32 µs<br>1.59x
Large<br>29,420 B<br>295.02 µs<br>449.00 µs<br>341.51 µs<br>1.52x
Very large<br>295,134 B<br>2.94 ms<br>4.45 ms<br>3.38 ms<br>1.51x
Across these inputs, the complete C pipeline was 1.51x to 5.39x faster than json.loads followed by the Rust-backed serializer. It was also 1.15x to 3.58x faster than the serializer-only call through Python, despite the C measurement including JSON parsing.
These are encouraging numbers, but they are not evidence that C is universally faster than Rust. This is not a standalone C-versus-Rust benchmark: the Rust implementation is a PyO3 extension designed to consume Python objects, so crossing the Python boundary is part of the product being measured. The benchmark was also run on one machine, with compact output and four generated fixtures. The right conclusion is narrower: for this workload, the smaller end-to-end C stack is faster than the Rust-backed Python path.
The full environment, harness, and reproduction commands are documented in the json2xml-c benchmark report.
A native tool, not another Python backend
Removing Python changed more than the implementation language. The C version owns the complete conversion pipeline and deliberately keeps the architecture small:
Parse length-bounded JSON bytes into an owned value tree.
Render that tree as compact XML.
Optionally run a pretty-printing pass over the generated XML.
The parser and renderer handle every JSON value type, UTF-8 and Unicode escapes, XML 1.0 validation, CDATA, configurable wrappers, type attributes, the special @attrs/@val/@flat keys, and W3C XPath 3.1 output. The public library interface is still only a small options structure, a conversion function, an explicit free function, and an error contract.
There is no embedded Python and no third-party JSON parser. The implementation uses standard C, builds with strict warnings enabled, and exposes both a library and the json2xml-c command:
json2xml-c --no-pretty -s '{"name":"Ada","active":true}'<br>json2xml-c data.json -o data.xml<br>cat data.json | json2xml-c -<br>json2xml-c --xpath data.json
The dependency-free boundary also creates useful constraints. For example, the C command intentionally does not fetch URLs: HTTPS would add a TLS dependency, and applications already have better ways to retrieve remote data before passing its bytes to the converter.
The first release ships ready-to-run CLI archives for Linux, macOS, and Windows on both x86-64 and ARM64. Linux executables are statically linked, and every archive is published with a SHA-256 checksum. The practical result is simple: download one file, put the executable on PATH, and run it without installing Python, Rust, or a package environment.
Performance is only useful if the code is trustworthy
C gives the implementation very little protection by default, so...