Skip to main content

xml2js vs fast-xml-parser vs xast: XML Parsing in Node.js (2026)

·PkgPulse Team

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

PackageWeekly DownloadsXPathASTStreamingBuilder
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

Featurexml2jsfast-xml-parserxast
ApproachXML → JS objectXML → JS objectXML → AST
PerformanceModerateFast (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.

Compare data processing and parsing packages on PkgPulse →

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.