xml2js parse and build round trip: working versions (tested 2026-10-16)
Parse XML with attributes and repeated elements using xml2js parseStringPromise (explicitArray false, mergeAttrs), then rebuild XML with Builder and assert the parsed object and output, fully offline.
passed2 passed · 0 failed2026-10-16 19:55 UTC- Status
- passed
- Last passed
- 2026-10-16 19:55 UTC
- Tests
- 2 / 2
- Language
- TypeScript
- Runtime
- node v22.23.3
pinned versions4
xml2js@0.6.2@types/xml2js@0.4.14@types/node@26.6.5tsx@4.23.15
run it
npm install --no-audit --no-fund --ignore-scripts=false npm test
test environment
node v22.23.3 linux 6.17.0-1022-azure x64 runner: github-actions 38958328921
The code
Exactly the files that passed. Your agent can pull them with get_recipe("xml2js-parse-build") (install), or see them on GitHub.
package.json
{
"name": "xml2js-parse-build",
"private": true,
"type": "module",
"scripts": {
"test": "node --import tsx --test test/*.test.ts"
},
"dependencies": {
"xml2js": "0.6.2"
},
"devDependencies": {
"@types/node": "26.6.5",
"@types/xml2js": "0.4.14",
"tsx": "4.23.15"
}
}
src/convert.ts
import { Builder, parseStringPromise } from "xml2js";
export type Book = {
id: string;
lang: string;
title: string;
author: string | string[];
};
export type Catalog = {
catalog: {
book: Book[];
};
};
export const parserOptions = {
explicitArray: false,
mergeAttrs: true,
} as const;
export async function parseCatalog(xml: string): Promise<Catalog> {
return (await parseStringPromise(xml, parserOptions)) as Catalog;
}
export function buildCatalog(catalog: Catalog): string {
const builder = new Builder({ headless: true, renderOpts: { pretty: false } });
const books = catalog.catalog.book.map(({ id, lang, ...rest }) => ({
$: { id, lang },
...rest,
}));
return builder.buildObject({ catalog: { book: books } });
}
test/convert.test.ts
import { test } from "node:test";
import assert from "node:assert/strict";
import { parseCatalog, buildCatalog } from "../src/convert.ts";
const xml = `<?xml version="1.0" encoding="UTF-8"?>
<catalog>
<book id="b1" lang="en">
<title>Dune</title>
<author>Frank Herbert</author>
<author>Kevin J. Anderson</author>
</book>
<book id="b2" lang="fr">
<title>Le Petit Prince</title>
<author>Antoine de Saint-Exupery</author>
</book>
</catalog>`;
test("parses attributes and repeated elements", async () => {
const parsed = await parseCatalog(xml);
assert.equal(parsed.catalog.book.length, 2);
assert.equal(parsed.catalog.book[0].id, "b1");
assert.equal(parsed.catalog.book[0].lang, "en");
assert.equal(parsed.catalog.book[0].title, "Dune");
assert.deepEqual(parsed.catalog.book[0].author, ["Frank Herbert", "Kevin J. Anderson"]);
assert.equal(parsed.catalog.book[1].title, "Le Petit Prince");
assert.equal(parsed.catalog.book[1].author, "Antoine de Saint-Exupery");
});
test("builds XML back from parsed data", async () => {
const parsed = await parseCatalog(xml);
const output = buildCatalog(parsed);
assert.ok(output.includes("<book"));
assert.ok(output.includes('id="b1"'));
assert.ok(output.includes("<title>Dune</title>"));
assert.ok(output.includes("<author>Frank Herbert</author>"));
assert.ok(output.includes("<author>Kevin J. Anderson</author>"));
assert.ok(output.includes("<title>Le Petit Prince</title>"));
assert.equal((output.match(/<book/g) ?? []).length, 2);
});
Connect to ProvenPin
Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.
terminal
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp
~/.cursor/mcp.json
{ "mcpServers": { "provenpin": { "url": "https://mcp.provenpin.com/mcp" } } }.vscode/mcp.json
{ "servers": { "provenpin": { "type": "http", "url": "https://mcp.provenpin.com/mcp" } } }~/.codeium/windsurf/mcp_config.json
{ "mcpServers": { "provenpin": { "serverUrl": "https://mcp.provenpin.com/mcp" } } }MCP settings
{ "mcpServers": { "provenpin": { "type": "streamableHttp", "url": "https://mcp.provenpin.com/mcp" } } }Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.