fast-xml-parser
fast-xml-parser converts XML to plain JavaScript objects and back, in pure JS with no C or C++ bindings. You construct an XMLParser with options and call parse(); XMLBuilder does the reverse and XMLValidator checks syntax. It handles big documents (tested to 100MB per the README), XML and HTML entities, DOCTYPE entities, unpaired HTML tags like br, stop nodes such as script, and can preserve tag order when round-tripping matters. It works in CommonJS, ESM, and the browser, which is why the AWS SDK and a long list of tools ship it as their XML layer.
The default pure-JS XML library, and the right pick for API payloads and round-tripping as long as you configure isArray and number parsing on day one instead of after the first production surprise. Go to sax for streaming and a schema validator when compliance matters.
Use it if
- You need XML to object conversion without native builds: it installs anywhere Node or a browser runs, no node-gyp, no libxml
- You are parsing API responses (SOAP, RSS, S3-style XML) where you want a plain object out and do not care about a DOM
- You need round-tripping: XMLBuilder plus preserveOrder can regenerate XML close to the input, which most parse-only libraries cannot
- You are parsing HTML-ish content and need unpaired tags, stop nodes, and HTML entities handled without a full HTML parser
- You need streaming or SAX-style parsing of files that do not fit in memory: fast-xml-parser loads the whole string; the project itself points to a separate SAX package for that
- You need spec-grade validation: the built-in validator checks syntax only, there is no XSD or DTD schema validation, and the README now recommends a separate validator package over the built-in one
- The single-vs-array trap will hurt you: a tag appearing once parses to an object and twice to an array, so untyped consumers break in production the first time a list has one item, unless you configure isArray up front
- Numeric fidelity matters: tag values are run through number parsing by default, so IDs like 0123 or 19-digit order numbers can come out mangled unless you turn parseTagValue off or tune numberParseOptions
- You want a W3C DOM with querySelector-style traversal: this produces plain objects; use a DOM library instead
Setup reality
npm install fast-xml-parser and you are parsing in three lines; CJS, ESM, and a CDN browser build all work, and v5 kept the v4 API so upgrades are mild. The real setup cost is option tuning: attributes are ignored by default until you set ignoreAttributes false, single occurrences of a repeated tag collapse to an object until you pass isArray, and number coercion needs numberParseOptions or parseTagValue false for anything that looks numeric but is not. Since v5 the package pulls in half a dozen sibling dependencies (strnum, fast-xml-builder, entity handling) where v4 was nearly self-contained, which some teams notice in audits. Expect to read the v4/v5 options doc once, seriously.
Patterns
Parse XML to a plain objectparse-basic
import { XMLParser } from 'fast-xml-parser';
const parser = new XMLParser();
const obj = parser.parse('<note><to>Ana</to><body>Call me</body></note>');
// { note: { to: 'Ana', body: 'Call me' } }Attributes are silently dropped by default; if the input has any, you almost always want the ignoreAttributes: false setup from the next pattern.
Keep attributes when parsingparse-attributes
import { XMLParser } from 'fast-xml-parser';
const parser = new XMLParser({
ignoreAttributes: false,
attributeNamePrefix: '@_'
});
const obj = parser.parse('<item id="42" type="book">XML Guide</item>');
// { item: { '#text': 'XML Guide', '@_id': 42, '@_type': 'book' } }Attribute keys get the @_ prefix so they cannot collide with child tag names; text alongside attributes lands under #text (configurable via textNodeName).
Force repeatable tags to always be arraysforce-array
import { XMLParser } from 'fast-xml-parser';
const parser = new XMLParser({
isArray: (name, jpath) => jpath === 'order.items.item'
});
const obj = parser.parse('<order><items><item>one</item></items></order>');
// obj.order.items.item is ['one'], not 'one'This is the number one production bug with this library: one occurrence parses to an object, two to an array. List every repeatable tag here before shipping.
Build XML from an objectbuild-xml
import { XMLBuilder } from 'fast-xml-parser';
const builder = new XMLBuilder({
format: true,
ignoreAttributes: false,
attributeNamePrefix: '@_'
});
const xml = builder.build({ item: { '@_id': 42, name: 'XML Guide' } });Builder options mirror parser options; use the same attributeNamePrefix on both sides or attributes silently become child tags on the way back out.
Validate syntax before parsingvalidate-xml
import { XMLValidator } from 'fast-xml-parser';
const result = XMLValidator.validate('<a>unclosed', {
allowBooleanAttributes: true
});
if (result !== true) {
console.error(result.err.code, result.err.msg, 'line', result.err.line);
}Returns literally true or an error object, so check result !== true, not truthiness. This is syntax-only; there is no XSD or DTD validation, and the README now recommends the separate fast-xml-validator package.
Round-trip XML preserving tag orderroundtrip-preserve-order
import { XMLParser, XMLBuilder } from 'fast-xml-parser';
const options = { ignoreAttributes: false, preserveOrder: true };
const parsed = new XMLParser(options).parse(inputXml);
const rebuilt = new XMLBuilder(options).build(parsed);preserveOrder changes the output shape to an array of single-key objects, which is much more awkward to traverse; only use it when you must regenerate XML faithfully.
Stop numeric strings from being mangledcontrol-number-parsing
import { XMLParser } from 'fast-xml-parser';
const parser = new XMLParser({
numberParseOptions: {
leadingZeros: false,
hex: false,
skipLike: /[0-9]{15,}/
}
});
const obj = parser.parse('<ids><a>0123</a><b>9007199254740993123</b></ids>');By default values that look numeric are converted, so 19-digit IDs lose precision and 0123 drops its zero. skipLike keeps matching values as strings; parseTagValue: false turns coercion off entirely.
Skip parsing inside script-like tagsstop-nodes
import { XMLParser } from 'fast-xml-parser';
const parser = new XMLParser({
stopNodes: ['*.script', '*.pre']
});
const obj = parser.parse(htmlish);
// script and pre contents arrive as raw unparsed stringsStop nodes capture inner content verbatim instead of recursing, which is how you survive embedded JS, CDATA-heavy blobs, or markup you plan to hand to another parser.
Parse HTML-ish content with unpaired tagsparse-html-unpaired-tags
import { XMLParser } from 'fast-xml-parser';
const parser = new XMLParser({
unpairedTags: ['br', 'hr', 'img', 'meta'],
htmlEntities: true,
stopNodes: ['*.script', '*.style']
});
const obj = parser.parse('<p>first line<br>second & third</p>');Without unpairedTags a bare br makes everything after it a child of br. This is still not a real HTML parser; malformed tag soup will produce surprising trees.
Keep CDATA and comments distinctcapture-cdata-comments
import { XMLParser } from 'fast-xml-parser';
const parser = new XMLParser({
cdataPropName: '__cdata',
commentPropName: '__comment'
});
const obj = parser.parse('<doc><!-- note --><val><![CDATA[5 < 6]]></val></doc>');Without cdataPropName the CDATA text is merged into the normal text value and you cannot rebuild it as CDATA later; set both options if you round-trip.
Handle entities without expansion surprisesentity-safety
import { XMLParser } from 'fast-xml-parser';
const parser = new XMLParser({
processEntities: false
});
const obj = parser.parse(untrustedXml);processEntities is on by default and DOCTYPE entities are supported, so parsing hostile XML with default settings exposes you to entity-expansion tricks; turn it off for untrusted input or size-limit the payload first.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| xml2js | npm | You want the older battle-tested callback-style converter and do not care about speed or building XML with order preserved. |
| sax | npm | You must stream gigantic XML without holding it in memory and can hand-write event handlers. |
| txml | npm | You want a tiny, very fast DOM-like tree and are willing to trade away entity and edge-case handling. |