xml2js vs fast-xml-parser vs xast: XML Parsing in Node.js (2026)
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.)
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.