camaro and template-driven XML to JSON in Node.js
camaro is a Node.js library that transform XML to JSON, using Node.js binding to native XML parser pugixml, one of the fastest XML parser around.
At a glance
- What is it?
- camaro is a Node.js library that projects an XML document onto a shape you describe in a template, rather than parsing the whole document into a generic tree. It reaches that speed by compiling a pugixml fork to WebAssembly, and the honest limitation is that it only transforms the fields you name.
- Who is it for?
- camaro is a good fit for a Node.js service that pulls a large XML feed and needs a handful of specific values out of it, because the template returns exactly the fields you describe instead of a generic tree you have to traverse and prune. It is a poor fit for small XML payloads, since the README says performance there will probably be worse than pure JavaScript parsers, and a poor fit when you need the entire document converted, since unmapped nodes are simply not extracted.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 41 days ago.
- What is it written in?
- Mainly C++, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.
Editorial analysis
A projection, not a conversion
Most XML to JSON libraries give you the whole document as a nested object and leave you to find what you need inside it. camaro takes the opposite approach. The README describes it as transforming XML to JSON by transforming only the properties you are interested in, with output that is ready to use as a JS object.
That distinction is the whole design. Instead of describing the shape of the input, you describe the shape of the output, and the library does the work of finding the corresponding nodes. The author's own explanation of why the project exists is that most of the time he is interested in some of the data in the whole XML mess, which is a common situation when you are integrating with a large third-party API that returns a full resource graph whether you asked for it or not.
The template is an array of pairs, where the first element is an XPath expression that selects nodes and the second describes the shape of each selected element. The example in the README selects every player node and then describes four fields for each one. A field can be an attribute, an element, or the result of a function applied to either.
So the output is not a faithful representation of the input. It is a purpose-built object that happens to come from XML. That is the trade camaro asks you to accept, and for a service whose job is to read three fields out of a large response, it is a trade that pays.
The audience is a Node.js developer with an XML feed they do not control and a fixed idea of the handful of values they need from it.
pugixml compiled to WebAssembly, shipped prebuilt
The parsing work is done by pugixml, a fast C++ XML parser, reached through the project's own fork of it. The fork exists because the library needs to reach into the parser's internals to evaluate the template expressions, which a stock pugixml build does not expose.
The delivery mechanism is unusual. The README says the library is written in C++ and compiled down to WebAssembly, so no re-compilation is needed by the user, and it works on all major platforms, Linux, macOS and Windows. The published package carries both a native entry point and a WebAssembly build, and the package.json file list includes dist/camaro.js and dist/camaro.wasm alongside the JavaScript wrappers.
That arrangement has a practical consequence for installation. There is no node-gyp compile step, no C++ toolchain on the user's machine and no build failure on an unsupported platform. The wasm artefact is fetched with the package and executed through WebAssembly, which is why the emscripten tag appears on the repository along with the wasm and webassembly ones.
The trade is that a WebAssembly boundary is not free. Calls across it have overhead, and that overhead is why the README notes that performance on small XML strings will probably be worse than pure JavaScript implementations. The library is built for large documents, and the wasm module is the price of not asking users to compile anything.
For anyone who does want to build from source, the repository carries build.sh, a napa-based dependency step exposed as the install-deps script, and a flake.nix with flake.lock for a reproducible Nix build, along with .clang-format and a prettier configuration for the two languages involved.
The template dialect: XPath 1.0 with a few extras
The template format is powered by XPath, and the README describes it as pretty much vanilla XPath 1.0 with some additions.
The additions are small and worth knowing precisely, because they are the parts that will not behave the way an XPath habit expects. A path that starts with a hash sign is a constant, so a template value of #1234 returns 1234. An empty path returns blank. And there is a set of string manipulation functions that XPath 1.0 does not have: lower-case, upper-case, title-case, camel-case, snake-case, string-join and raw.
The author's framing of that list is that these are the ones he needs for now, with the hope of eventually adding all XPath 2.0 functions, and an invitation for pull requests. So the dialect is knowingly incomplete, and it is incomplete in a specific direction: it is a superset of XPath 1.0 for the subset of string operations a template author reaches for often.
The example in the README shows the composition in practice. The name field applies title-case to the name element, the jerseyNumber field reads the jerseyNumber attribute and lets the numeric conversion happen implicitly, the yearOfBirth field wraps the element in the number function to cast it, and the isRetired field uses boolean with a comparison against the string true. The resulting object contains a number, not a string, for both numeric fields, and a real boolean for the flag.
Two details follow from that. First, the type of the output field is determined by the expression, not by the template declaration, so you can get a string where you expected a number without any error. Second, because the expressions are evaluated against the node, a path that matches nothing yields nothing, and nothing is not an error either. Both behaviours make a template easy to write and easy to get subtly wrong at the same time.
Installing camaro and running a first transform
Installation is a single package add, and the README gives both package managers:
pnpm add camaro
# npm install camaroThere is nothing else to do. The package declares node >= 20.x as its engine requirement, and it ships prebuilt artefacts, so there is no native build to trigger.
The usage example in the README requires two functions, transform and prettyPrint, both of which are asynchronous:
const template = ['players/player', {
name: 'title-case(name)',
jerseyNumber: '@jerseyNumber',
yearOfBirth: 'number(yearOfBirth)',
isRetired: 'boolean(isRetired = "true")'
}]
;(async function () {
const result = await transform(xml, template)
console.log(result)
})()The transform call takes the XML source and the template and resolves to the transformed object. The second argument of transform can be an object or an array depending on whether you want a single object or an array of objects, and the array form shown here produces one element per selected node.
The other exported function, prettyPrint, reformats the source XML with an indent size, which is a debugging convenience rather than part of the transform path. The README also shows it taking an options object with indentSize set to 4.
For the full API, the README points to API.md in the repository, and additional examples sit in the examples/ directory, including array.md, default-value.md and raw-text.md alongside the sample XML files simple.xml, recipe.xml and ean.xml. There is also a wasm.html file, which is a browser-side demonstration of the same library running as WebAssembly outside Node.
The test suite runs on tape with the script test set to tape test/*.test.js, and formatting is checked separately for C++ with clang-format and for JavaScript with prettier, which is the arrangement a mixed C++ and JavaScript project usually settles into.
Multi-core scaling through a worker thread pool
The README lists scaling well with multi-core processors by the use of a worker_threads pool, and the repository layout shows how that is put together. There is a lean-pool.js, a pool-worker.js, a worker.js, a sab-ipc.js and a json-parse.js at the root, alongside the main index.js and index.mjs entry points.
The file names describe the design. sab-ipc.js points to shared array buffer communication between workers, lean-pool.js to a small pool implementation, and json-parse.js to JSON parsing done outside the main thread. The pool is described as lean for a reason: a worker pool that exists only to run a native parser should not carry a framework's worth of overhead.
The reason a pool is needed at all is that the wasm module is single-threaded. Parsing one large document in one worker is a straightforward win, and splitting work across threads is what lets a multi-core machine absorb a large document that would otherwise occupy one core.
The published file list in package.json includes all five of those modules, so they ship as part of the package rather than being built at install time. The package also has a exports map that points require at index.js, import at index.mjs and types at index.d.ts, so the library is usable from both CommonJS and ES modules with TypeScript declarations available either way.
For a reader deciding whether the pool is worth it, the practical point is that it is a separate code path with its own failure modes. A worker pool means messages, serialisation boundaries and the possibility of a worker dying mid-document, and none of that appears in the README's usage example, which calls transform without any pool option.
The benchmark, and where camaro is the wrong choice
The README carries a benchmark table comparing camaro v6 against txml, fast-xml-parser, xml2js and xml-js, using an actual XML response from the Expedia API with some nodes deleted to vary the size. The summary gives the factors as 5.27x faster than txml, 25.78x faster than xml-js, 24.21x faster than fast-xml-parser and 37.02x faster than xml2js, with an average of 98.94 microseconds per iteration for camaro against 491.71 for txml and figures in the millisecond range for the three JavaScript parsers.
Two caveats come with it, and the author is explicit about both. The first is that this is described as an unfair game for camaro because it only transforms the fields specified in the template, while the comparison libraries convert whole documents. The second is that performance on small XML strings will probably be worse than pure JavaScript implementations, with the conclusion that if your use case consists of small XML strings only, you probably do not need this library.
That second caveat decides the question for a large class of users. If you are parsing a few hundred bytes of XML inside a request handler, a pure JavaScript parser is simpler, has no wasm boundary and no worker pool, and the README tells you as much. The library earns its design cost on large documents and on feeds where you only want a slice of the data.
The completeness limitation is the other side of the same trade. A template describes the output, so anything you did not describe is not extracted, and there is no mode that turns the whole document into a generic tree. If your consumer needs the complete resource graph, camaro is the wrong tool regardless of how fast it is.
The repository does carry a complete benchmark directory with the scripts bench:all, bench:baseline and bench:to-json wired to node benchmark/transform.js, node benchmark/baseline.js and node benchmark/to-json.js, so the comparison can be reproduced rather than taken on trust.
Releases, licence and the alternative the author recommends
camaro is MIT licensed, with the LICENSE file at the repository root and the licence field in package.json, which is permissive enough for commercial use and for embedding in a service you ship.
The repository is not archived, and the last push was on 2026-09-11, the same day as the v6.7.3 release. The preceding releases were v6.7.2 on 2026-08-13 and v6.7.1 on 2026-08-11, so the release line is moving regularly. The default branch is develop rather than main, which is worth knowing if you clone the repository to build from source, because the published package is what npm serves and the branch layout is a development convention rather than something that affects consumers.
The README names one alternative, and it does so with unusual candour. It points to cruftless by wspringer and says the author personally finds it fascinating and that its template engine is more powerful than camaro's XPath-based approach, recommending it directly.
The difference is real and specific. cruftless uses a template engine of its own design rather than evaluating XPath expressions against the document, so its expressiveness does not stop at XPath 1.0 plus a short list of string functions. camaro's advantage is the opposite property: its expressions are XPath, which means a large body of existing knowledge and tooling applies to them directly. If your transformation logic already exists as XPath, or your team knows XPath well, that familiarity is worth a real amount.
The project is also used in the wild by other tools, among them the Hexo generators for sitemaps and feeds and the WordPress migrator for Hexo, plus the academic-bloggers-toolkit, which tells you the library holds up under a static site generator's build process rather than only in a request handler.
Editorial conclusion
camaro is a good fit for a Node.js service that pulls a large XML feed and needs a handful of specific values out of it, because the template returns exactly the fields you describe instead of a generic tree you have to traverse and prune. It is a poor fit for small XML payloads, since the README says performance there will probably be worse than pure JavaScript parsers, and a poor fit when you need the entire document converted, since unmapped nodes are simply not extracted. Before adopting, check that the upstream XML stays stable, since a template that assumes an element path fails quietly when the feed changes, and read the API.md reference before assuming XPath 2.0 behaviour, because the custom functions are the ones that differ from a stock XPath engine.
Frequently asked questions
How do I install camaro?
Run `pnpm add camaro`, or `npm install camaro`. There is no compilation step, since the C++ parser is compiled to WebAssembly and shipped prebuilt, and the package requires Node 20 or newer.
How does the camaro template syntax work?
A template is an array whose first item is an XPath expression selecting nodes and whose second item describes the fields to extract from each node. Fields can be attributes with the @ prefix, elements, or function calls such as title-case, number and boolean, with a path starting with # treated as a constant.
Is camaro faster than pure JavaScript XML parsers?
On large XML documents, yes. The README benchmark reports camaro v6 at 98.94 microseconds per iteration against 2.49 to 3.58 milliseconds for xml-js, fast-xml-parser and xml2js, but notes the comparison is unfair because camaro only transforms the fields named in the template.
When should I not use camaro?
For small XML strings, where the README says performance will probably be worse than pure JavaScript implementations, and when you need the whole document converted rather than a selected subset, since camaro extracts only the fields you describe in the template.
What licence is camaro released under?
MIT, with the LICENSE file in the repository root and the licence field set in package.json. The most recent release is v6.7.3 from 2026-09-11, and the last push was on 2026-09-11.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/tuananh-camaro)Was this useful?