TL;DR
xml2js is the most popular XML parser — it converts XML into a JavaScript object tree, simple and battle-tested but with a quirky object structure that can surprise you. fast-xml-parser is 2-5x faster, supports XPath queries, and produces cleaner output with configurable parsing options. xast is the unified ecosystem's approach — an XML AST (Abstract Syntax Tree) for programmatic XML manipulation and transformation, not just parsing. For most XML needs (RSS feeds, SOAP, sitemaps): fast-xml-parser. For legacy code or simple cases: xml2js. For complex XML transformations or unified/rehype pipelines: xast.
Key Takeaways
- xml2js: ~20M weekly downloads — most popular, converts XML → JS objects, legacy-friendly
- fast-xml-parser: ~35M weekly downloads — faster, XPath, cleaner API, growing rapidly
- xast: ~2M weekly downloads — AST-based, unified ecosystem, programmatic XML manipulation
- fast-xml-parser is now more popular than xml2js and has a better API — prefer for new projects
- Both handle standard XML/XHTML — for HTML parsing, use cheerio or node-html-parser instead
- XML in 2026 is mostly RSS/Atom feeds, SOAP APIs, sitemaps, SVG, and configuration files
Download Trends
| Package | Weekly Downloads | XPath | AST | Streaming | Builder |
|---|---|---|---|---|---|
xml2js | ~20M | ❌ | ❌ | ❌ | ✅ |
fast-xml-parser | ~35M | ✅ | ✅ | ❌ | ✅ |
xast-util-from-xml | ~2M | ❌ | ✅ | ❌ | ✅ |
xml2js
xml2js — the classic XML-to-JavaScript converter:
Basic parsing
import xml2js from "xml2js"
const xml = `
<feed xmlns="http://www.w3.org/2005/Atom">
<title>PkgPulse Updates</title>
<entry>
<title>react 18.3.1 released</title>
<id>urn:pkgpulse:react:18.3.1</id>
<updated>2024-04-26T00:00:00Z</updated>
</entry>
<entry>
<title>vue 3.5.0 released</title>
<id>urn:pkgpulse:vue:3.5.0</id>
<updated>2024-09-03T00:00:00Z</updated>
</entry>
</feed>
`
// Async parse:
const result = await xml2js.parseStringPromise(xml)
console.log(result.feed.title[0]) // "PkgPulse Updates"
// Note: arrays everywhere — even single values come as arrays!
// Access entries:
const entries = result.feed.entry
for (const entry of entries) {
console.log(entry.title[0]) // "react 18.3.1 released"
console.log(entry.id[0]) // "urn:pkgpulse:react:18.3.1"
console.log(entry.updated[0]) // "2024-04-26T00:00:00Z"
}
The array problem (xml2js gotcha)
import xml2js from "xml2js"
// xml2js wraps everything in arrays — even single values:
const result = await xml2js.parseStringPromise("<root><name>react</name></root>")
// result.root.name = ["react"] ← array, not string!
// Fix with explicitArray: false:
const clean = await xml2js.parseStringPromise(xml, {
explicitArray: false, // Don't wrap single values in arrays
})
// clean.root.name = "react" ← string now
// But multi-value elements are still arrays:
const multi = await xml2js.parseStringPromise(`
<root><tag>a</tag><tag>b</tag></root>
`, { explicitArray: false })
// multi.root.tag = ["a", "b"] ← still array when multiple
Configuration options
import xml2js from "xml2js"
const result = await xml2js.parseStringPromise(xmlString, {
explicitArray: false, // Single values as strings, not arrays
explicitRoot: false, // Don't wrap in root element key
ignoreAttrs: false, // Include XML attributes (default: false = include them)
attrkey: "@", // Key for attributes (default: "$")
charkey: "_", // Key for text content (default: "_")
trim: true, // Trim whitespace from text nodes
normalize: true, // Normalize whitespace
mergeAttrs: true, // Merge attributes into the object (not as "$")
xmlns: false, // Include namespace info
})
XML builder
import xml2js from "xml2js"
const builder = new xml2js.Builder({
rootName: "package",
xmldec: { version: "1.0", encoding: "UTF-8" },
renderOpts: { pretty: true, indent: " " },
})
const xml = builder.buildObject({
name: "react",
version: "18.3.1",
downloads: 45_000_000,
tags: { tag: ["ui", "frontend", "component"] },
})
// <?xml version="1.0" encoding="UTF-8"?>
// <package>
// <name>react</name>
// <version>18.3.1</version>
// <downloads>45000000</downloads>
// <tags>
// <tag>ui</tag>
// <tag>frontend</tag>
// <tag>component</tag>
// </tags>
// </package>
fast-xml-parser
fast-xml-parser — 2-5x faster with a cleaner API:
Basic parsing
import { XMLParser } from "fast-xml-parser"
const parser = new XMLParser()
const xml = `
<feed>
<title>PkgPulse Updates</title>
<entry>
<title>react 18.3.1 released</title>
<id>urn:pkgpulse:react:18.3.1</id>
<updated>2024-04-26T00:00:00Z</updated>
</entry>
</feed>
`
const result = parser.parse(xml)
// Cleaner output — no array wrapping by default:
console.log(result.feed.title) // "PkgPulse Updates"
console.log(result.feed.entry.title) // "react 18.3.1 released"
Configuration options
import { XMLParser } from "fast-xml-parser"
const parser = new XMLParser({
ignoreAttributes: false, // Parse XML attributes (default: true = ignore)
attributeNamePrefix: "@_", // Prefix for attribute keys
allowBooleanAttributes: true, // <disabled/> → { disabled: true }
parseTagValue: true, // Auto-convert "true"/"false"/"123" → types
trimValues: true, // Trim whitespace
parseTrueNumberOnly: false, // Parse all number-like strings as numbers
isArray: (tagName) => { // Always treat these as arrays:
return ["entry", "item", "package"].includes(tagName)
},
})
// RSS feed with attributes:
const rss = `
<rss version="2.0">
<channel>
<item published="true">
<title>Post 1</title>
<link>https://pkgpulse.com/blog/1</link>
</item>
<item published="false">
<title>Post 2</title>
<link>https://pkgpulse.com/blog/2</link>
</item>
</channel>
</rss>
`
const feed = parser.parse(rss)
// feed.rss["@_version"] = "2.0" (attribute with prefix)
// feed.rss.channel.item = [...] (array because of isArray config)
XPath queries
import { XMLParser } from "fast-xml-parser"
const parser = new XMLParser({ ignoreAttributes: false })
const result = parser.parse(xmlData)
// fast-xml-parser doesn't have built-in XPath — use xpath package alongside it:
import xpath from "xpath"
import { DOMParser } from "@xmldom/xmldom"
const doc = new DOMParser().parseFromString(xmlData, "text/xml")
const nodes = xpath.select("//entry/title/text()", doc) as Attr[]
const titles = nodes.map((n) => n.nodeValue)
Sitemap parsing
import { XMLParser } from "fast-xml-parser"
import { readFile } from "fs/promises"
async function parseSitemap(url: string) {
const response = await fetch(url)
const xml = await response.text()
const parser = new XMLParser({
isArray: (tag) => tag === "url",
})
const sitemap = parser.parse(xml)
const urls = sitemap.urlset.url as Array<{
loc: string
lastmod?: string
changefreq?: string
priority?: number
}>
return urls.map((u) => ({
url: u.loc,
lastModified: u.lastmod ? new Date(u.lastmod) : undefined,
priority: u.priority ?? 0.5,
}))
}
// Usage:
const pages = await parseSitemap("https://www.pkgpulse.com/sitemap.xml")
// [{ url: "https://...", lastModified: Date, priority: 0.8 }, ...]
XML builder
import { XMLBuilder } from "fast-xml-parser"
const builder = new XMLBuilder({
ignoreAttributes: false,
format: true,
indentBy: " ",
suppressEmptyNode: true,
})
const obj = {
"?xml": { "@_version": "1.0", "@_encoding": "UTF-8" },
urlset: {
"@_xmlns": "http://www.sitemaps.org/schemas/sitemap/0.9",
url: [
{ loc: "https://pkgpulse.com/", priority: 1.0 },
{ loc: "https://pkgpulse.com/compare/react-vs-vue", priority: 0.8 },
],
},
}
const xml = builder.build(obj)
xast (unified ecosystem)
xast — XML Abstract Syntax Tree for the unified ecosystem:
Parse XML to AST
import { fromXml } from "xast-util-from-xml"
import { toXml } from "xast-util-to-xml"
import { visit } from "unist-util-visit"
import type { Element, Text } from "xast"
const xml = `
<packages>
<package deprecated="false">
<name>react</name>
<downloads>45000000</downloads>
</package>
<package deprecated="true">
<name>moment</name>
<downloads>12000000</downloads>
</package>
</packages>
`
// Parse to AST:
const tree = fromXml(xml)
// Visit and transform:
const packageNames: string[] = []
visit(tree, "element", (node: Element) => {
if (node.name === "name") {
const textNode = node.children[0] as Text
packageNames.push(textNode.value)
}
})
console.log(packageNames) // ["react", "moment"]
Transform XML
import { fromXml } from "xast-util-from-xml"
import { toXml } from "xast-util-to-xml"
import { visit } from "unist-util-visit"
import { remove } from "unist-util-remove"
import type { Element } from "xast"
// Remove deprecated packages from XML:
const tree = fromXml(xmlData)
remove(tree, (node) => {
if (node.type !== "element") return false
const el = node as Element
return el.name === "package" && el.attributes.deprecated === "true"
})
// Serialize back to XML:
const cleaned = toXml(tree)
Feature Comparison
| Feature | xml2js | fast-xml-parser | xast |
|---|---|---|---|
| Approach | XML → JS object | XML → JS object | XML → AST |
| Performance | Moderate | Fast (2-5x) | Moderate |
| Attribute handling | ✅ | ✅ | ✅ |
| Array handling | ⚠️ Quirky | ✅ Configurable | ✅ |
| XML building | ✅ | ✅ | ✅ |
| XPath | ❌ | ❌ (external) | ❌ |
| AST traversal | ❌ | ❌ | ✅ unist-util-visit |
| Streaming | ❌ | ❌ | ❌ |
| TypeScript | ✅ | ✅ | ✅ |
| unified compatible | ❌ | ❌ | ✅ |
| Bundle size | ~150KB | ~100KB | ~50KB |
When to Use Each
Choose fast-xml-parser if:
- New projects needing XML parsing — cleaner API, faster, more configurable
- Parsing RSS/Atom feeds, sitemaps, SVG, SOAP responses
- You want type coercion and better attribute handling out of the box
- Performance matters (2-5x faster than xml2js for large files)
Choose xml2js if:
- Existing codebase already using it — no reason to migrate
- Simple XML-to-object conversion for legacy integration
- You're familiar with the
$/_attribute convention
Choose xast if:
- You're in the unified/remark/rehype ecosystem
- You need to programmatically transform XML (not just parse and read)
- Building XML processing pipelines with composable plugins
- You need AST traversal patterns similar to what you use for HTML/markdown
For HTML (not XML):
- Use cheerio, node-html-parser, or @xmldom/xmldom — they handle malformed HTML
- XML parsers are strict — they fail on invalid HTML (unclosed tags, etc.)
XML Namespaces and SOAP Integration
One of the most challenging aspects of production XML parsing is namespace handling. XML namespaces allow the same element name to mean different things in different contexts — <title> in an Atom feed namespace means something different than <title> in an XHTML namespace. xml2js provides namespace information when xmlns: true is set in options, but working with the resulting namespace metadata requires understanding the internal representation. fast-xml-parser handles namespaces through configurable prefix stripping and namespace-aware attribute parsing, though complex namespace-qualified attribute names require careful option configuration. SOAP APIs are a common production XML use case that requires correct namespace handling — SOAP envelopes use the http://schemas.xmlsoap.org/soap/envelope/ namespace, and inner elements use service-specific namespaces. Both xml2js and fast-xml-parser can handle SOAP responses when configured correctly, but testing against the actual SOAP service responses is essential since namespace handling edge cases vary by configuration. xast's AST model preserves namespaces exactly as they appear in the source document, making it the most reliable for namespace-sensitive transformations.
Security Considerations for XML Parsing
XML has a longer history of security vulnerabilities than JSON, and several attack classes are worth knowing when parsing untrusted XML. XML External Entities (XXE) attacks allow a malicious XML document to reference external files or URLs through entity declarations — a common attack vector against SOAP APIs and XML file upload endpoints. xml2js is not vulnerable to XXE by default because it uses a pure JavaScript parser (sax) that does not resolve external entities. fast-xml-parser similarly does not resolve external entities in its default configuration. The more relevant concern for Node.js XML parsing is billion-laughs / XML bomb attacks, where a deeply nested entity expansion causes exponential memory consumption. Both libraries have configurable nesting depth limits to prevent this. For XML received from public APIs or user uploads, validating the document size before parsing and setting conservative depth limits are important defensive measures. xast parses XML to an AST and inherits whatever security properties the underlying vfile processing pipeline provides.
Streaming XML Parsing for Large Documents
Neither xml2js nor fast-xml-parser supports streaming XML parsing in their standard APIs — both parse the entire document into memory before returning results. For large XML files (multi-megabyte export files, large sitemaps with 50,000+ URLs, XML data lake files), this memory usage can be significant. The streaming XML parsing approach in Node.js uses SAX (Simple API for XML) parsing, where the parser fires events for each element start/end rather than building a complete tree. The sax npm package is the most widely used SAX parser and is actually used by xml2js under the hood. For applications that need to process XML records without loading the entire document — think processing 100MB XML product catalog files — SAX-style streaming is the right approach, even if it requires more code than the high-level APIs of xml2js or fast-xml-parser. xast's unified pipeline model can be adapted to streaming for very large documents through the vfile interface, though this requires more setup than using SAX directly.
XML in 2026 and the Maintenance Question
The question of whether XML libraries are actively maintained matters given the niche use case. xml2js saw its last major release in 2023 (v0.6.x), and while it remains functional and widely installed, it is not receiving feature development. fast-xml-parser is actively maintained with regular releases and is on a clear upward trajectory in downloads, having surpassed xml2js in weekly downloads in 2024 — a milestone that reflects the community voting for a better-maintained alternative. xast is maintained by the unified ecosystem, which has sustained high-quality maintenance across a broad package ecosystem for years, making it a reliable choice despite lower absolute download counts. For new projects that need XML parsing, fast-xml-parser is the clear recommendation — better API, faster, more configurable, and actively maintained. The only reason to start a new project with xml2js is familiarity or to match an existing codebase's conventions.
XML Validation and Schema Enforcement
Beyond parsing, production XML handling often requires validation against a schema. XML Schema Definition (XSD) allows specifying the structure, data types, and constraints of an XML document, and validating an incoming SOAP response or EDI document against its XSD catches data quality issues at the ingestion layer before they propagate to business logic. Neither xml2js nor fast-xml-parser support XSD validation natively — they parse XML structure but do not validate against schemas. For XSD validation in Node.js, the libxmljs2 package wraps the C-based libxml2 library and supports full XSD validation. The typical pattern is: validate with libxmljs2, then parse with fast-xml-parser for the cleaner JavaScript object representation. xast's AST model supports validation through the unified ecosystem's vfile plugin architecture, but XSD-level validation requires custom plugin implementation. For simple schema enforcement without full XSD, using Zod or similar validation libraries on the parsed JavaScript object (after fast-xml-parser converts XML to an object) is simpler and sufficient for most use cases.
Methodology
Download data from npm registry (weekly average, February 2026). Performance comparisons approximate for typical RSS/sitemap payloads. Feature comparison based on xml2js v0.6.x, fast-xml-parser v4.x, and xast-util-from-xml v4.x.
Compare data processing and parsing packages on PkgPulse →
See also: pm2 vs node:cluster vs tsx watch and h3 vs polka vs koa 2026, better-sqlite3 vs libsql vs sql.js.