mrkeyoor.com_
Sat 08 Aug 22:52 UTC
npmUtilsupdated 08 Aug 2026

xmldoc

xmldoc turns an XML string into a small typed tree of `XmlDocument`, `XmlElement`, text, CDATA, and comment nodes. It sits between raw SAX events and a browser-style DOM: parsing is synchronous and strict, attributes become a string dictionary, child elements have convenience searches, and simple dot paths can retrieve known nested values. It is best for predictable XML responses whose structure your code already knows. It is not XPath, a standards-complete DOM, an HTML parser, a streaming interface, or a production XML serializer.

Verdict

xmldoc is a pleasant small-tree parser for known XML on current ESM runtimes, with better types and maintenance than its age suggests. Do not install it for CommonJS, XPath, namespace-aware querying, huge feeds, HTML scraping, or XML generation.

API stability3/5The element methods and member names remain recognizably small and longstanding, and the v3 TypeScript declarations define them clearly. However, 3.0 is a genuine platform break: CommonJS support, the browser global, compatibility shims, and pre-Node-22 runtimes were removed. Version 2 had only recently added dual-module packaging. Data-level behavior also has sharp conventions around direct text, literal namespace prefixes, custom paths, and debug formatting, so callers should pin a major and test exact traversal assumptions.
Docs4/5The README explains the document and element model, every primary member, direct-child semantics, dot paths, attribute paths, formatting options, React Native extras, and the key warning that output is debug-only. The v3 changelog lists all runtime and module breaks plainly, and declarations cover recursive search plus node classes. It misses a dedicated security and resource-limits section, gives little guidance on mixed content, and recommends a now-missing `node-elementtree` repository for heavier searching, so edge cases still require source reading.
Maintenance4/5Version 3.0.0 was published on June 3, 2026, the same day as the latest repository push. The release rewrote the package around one TypeScript source, restored 51 tests, fixed a pre-root text regression, moved CI to the correct branch, updated `sax` to 1.6.0, and tests Node 22, 24, and 26. GitHub reports 9 open issues and PRs. The project is actively cared for, though its release history has long quiet periods and the major modernization substantially narrowed runtime compatibility.
Ecosystem4/5The npm endpoint reports 3,096,565 downloads for July 31 through August 6, 2026, which indicates extensive direct or transitive use despite only 274 GitHub stars. The package now fits typed ESM services well and depends on the familiar `sax` parser. Its ecosystem reach is constrained by Node 22+, ESM-only packaging, no browser-global artifact, and optional React Native polyfills. XML users needing XPath, namespaces, streams, builders, or object-mapping conventions will find stronger surrounding ecosystems elsewhere.

Use it if

  • You consume small or medium XML API responses with a known shape and want direct child and path helpers
  • You want strict XML parsing with source line, column, and character-position metadata on elements
  • Your project is ESM on Node 22 or newer and benefits from first-party TypeScript declarations
  • You need text, CDATA, comments, attributes, and element order preserved in a simple inspectable tree
Skip it if

Setup reality

Version 3 setup is easy only if the host is already modern: run `npm install xmldoc`, use `import { XmlDocument } from 'xmldoc'`, and pass a complete XML string to the constructor. The package is pure JavaScript with one runtime dependency, `sax` 1.6.x, and ships its own declarations, so there is no native compilation or type package. The hard boundaries are deliberate. Node 22 is the declared minimum, v3 is ESM-only, and the old browser-global build was removed. A CommonJS application must either migrate, use a dynamic import boundary, or pin the maintained API it knows to the 2.x line. React Native may require explicit `buffer` and `stream` installs according to the README. Parsing is synchronous and strict; an empty or malformed document throws during construction, so surround untrusted input with `try/catch` and apply your own byte or character limit before parsing. The complete tree stays in memory. Namespaces are not resolved: `<office:body>` is named `office:body`, and namespace declaration attributes stay in `attr`. Direct-child helpers do not recurse, dot paths select only the first matching child at each step, and the path language has none of XPath's predicates or namespace rules. `children` contains element, text, CDATA, and comment nodes, while `childNamed`, `childrenNamed`, and `eachChild` filter to elements. An element's `val` contains only its own text and CDATA, not the concatenated text of descendants. Finally, do not use `toString()` as a round-trip writer. It trims surrounding text by default, supports an HTML formatting mode, and is explicitly labeled debug-only. Use a real XML builder when output correctness is part of the contract.

Patterns

Parse a document in ESMparse-document

import { XmlDocument } from 'xmldoc';

const xml = '<catalog version="2"><book id="b1">XML Basics</book></catalog>';
const document = new XmlDocument(xml);

console.log(document.name);         // catalog
console.log(document.attr.version); // 2
console.log(document.childNamed('book')?.val); // XML Basics

xmldoc 3 is ESM-only and declares Node 22 or newer. CommonJS `require()` callers should not upgrade from 2.x without a module migration.

Catch invalid or empty XMLhandle-parse-errors

import { XmlDocument } from 'xmldoc';

function tryParseXml(input) {
  if (input.length > 1_000_000) return { ok: false, error: 'too large' };
  try {
    return { ok: true, document: new XmlDocument(input) };
  } catch (error) {
    return { ok: false, error: error instanceof Error ? error.message : String(error) };
  }
}

console.log(tryParseXml('<root><broken></root>'));

The constructor parses synchronously and throws on malformed XML. The sample size cap is an application policy, not a limit supplied by xmldoc.

Find the first direct child by namefind-direct-child

import { XmlDocument } from 'xmldoc';

const doc = new XmlDocument(`
  <order>
    <customer><name>Ada</name></customer>
    <status>paid</status>
  </order>
`);

const status = doc.childNamed('status');
if (!status) throw new Error('missing status');
console.log(status.val);

`childNamed()` checks direct children only and returns `undefined` when absent. It will not find the nested `name` element.

Collect repeated direct elementsfind-repeated-children

import { XmlDocument } from 'xmldoc';

const doc = new XmlDocument(`
  <items>
    <item sku="A">10</item>
    <item sku="B">20</item>
  </items>
`);

const items = doc.childrenNamed('item').map((item) => ({
  sku: item.attr.sku,
  quantity: Number(item.val),
}));
console.log(items);

Attributes and text values are strings. Convert numbers, dates, and booleans explicitly and validate the result.

Find a child with an attributefind-by-attribute

import { XmlDocument } from 'xmldoc';

const doc = new XmlDocument(`
  <users>
    <user id="42" role="admin">Ada</user>
    <user id="43">Lin</user>
  </users>
`);

const exact = doc.childWithAttribute('id', '42');
const hasRole = doc.childWithAttribute('role');
console.log(exact?.val, hasRole?.attr.role);

This searches only direct child elements and returns the first match. Omit the value to test for a truthy attribute value.

Read a nested value or attribute with a pathread-known-path

import { XmlDocument } from 'xmldoc';

const doc = new XmlDocument(`
  <book><author><name id="a1">Ursula Le Guin</name></author></book>
`);

console.log(doc.valueWithPath('author.name'));    // Ursula Le Guin
console.log(doc.valueWithPath('author.name@id')); // a1
console.log(doc.descendantWithPath('author.name')?.name); // name

Dot paths are a small xmldoc convention, not XPath. They select the first matching child at each level and support no predicates or array indexes.

Recursively find descendants by literal namesearch-descendants

import { XmlDocument } from 'xmldoc';

const doc = new XmlDocument(`
  <feed><section><entry>A</entry></section><entry>B</entry></feed>
`);

const entries = doc.descendantsNamed('entry');
console.log(entries.map((entry) => entry.val)); // ['A', 'B']

Recursive search walks the in-memory tree and matches literal names. For many complex searches or very large inputs, use a selector-aware or streaming parser.

Address prefixed names literallyhandle-namespaces

import { XmlDocument } from 'xmldoc';

const doc = new XmlDocument(`
  <feed xmlns:atom="http://www.w3.org/2005/Atom">
    <atom:entry atom:id="7">Hello</atom:entry>
  </feed>
`);

const entry = doc.childNamed('atom:entry');
console.log(entry?.attr['atom:id']); // 7
console.log(doc.attr['xmlns:atom']);

xmldoc does not resolve namespace URIs. Prefixes remain in element and attribute names, so documents that choose a different prefix need application handling.

Distinguish elements, text, CDATA, and commentsinspect-mixed-content

import { XmlDocument } from 'xmldoc';

const doc = new XmlDocument(
  '<root>before<em>inside</em><![CDATA[raw <text>]]><!-- note --></root>',
);

for (const node of doc.children) {
  if (node.type === 'element') console.log('element', node.name);
  if (node.type === 'text') console.log('text', node.text);
  if (node.type === 'cdata') console.log('cdata', node.cdata);
  if (node.type === 'comment') console.log('comment', node.comment);
}

`children` includes every node type. Element-only helpers filter them out, and the parent `.val` contains only direct text and CDATA, not descendant element text.

Iterate direct child elements with early exititerate-elements

import { XmlDocument } from 'xmldoc';

const doc = new XmlDocument('<root>text<a/><b stop="yes"/><c/></root>');

doc.eachChild((child, index) => {
  console.log(index, child.name);
  if (child.attr.stop === 'yes') return false;
});

Returning `false` stops iteration. The callback index refers to the full `children` array, so text or comment nodes can make element indexes non-contiguous.

Attach source positions to validation errorsreport-source-position

import { XmlDocument } from 'xmldoc';

const doc = new XmlDocument('<root>
  <item id=""/>
</root>');
const item = doc.childNamed('item');

if (item && !item.attr.id) {
  throw new Error(`item id is empty near line ${item.line}, column ${item.column}`);
}

Elements expose line, column, character position, and start-tag position from the SAX parser. Confirm whether your user-facing line numbering should add one.

Format a parsed subtree for logsformat-for-debugging

import { XmlDocument } from 'xmldoc';

const doc = new XmlDocument('<root><message>  a long message for diagnostics only  </message></root>');

console.log(doc.toString());
console.log(doc.toString({ compressed: true, preserveWhitespace: true }));
console.log(doc.toString({ trimmed: true }));

The README says `toString()` is for debugging and does not guarantee valid XML. It trims surrounding text unless `preserveWhitespace` is set, so do not use it as a persistence serializer.

Alternatives

PackageRegistryPick it when
fast-xml-parsernpmChoose it for configurable XML-to-object parsing, validation, and XML building without a native dependency
xml2jsnpmChoose it when an established callback or Promise-based XML-to-plain-object mapping fits existing CommonJS code
saxesnpmChoose it for event-driven parsing, namespace awareness, and control over memory instead of an eagerly built document tree