A fast, dependency-free XML parser, builder, and validator for TypeScript and Bun. Character-by-character parsing with charCodeAt comparisons for maximum performance.
- Zero Dependencies - only Bun as a runtime
- XMLParser - parse XML strings to JavaScript objects
- XMLBuilder - build XML strings from JavaScript objects
- XMLValidator - validate XML structure with detailed error reporting
- Entity Handling - XML, HTML, numeric, and hex entity decoding/encoding
- Namespace Support - optional namespace prefix removal
- Preserve Order - array-based output to maintain element ordering
- Stop Nodes - skip parsing of specific tag contents
- Unpaired Tags - support for HTML-style void elements (e.g.,
<br>,<hr>) - CDATA & Comments - optionally capture CDATA sections and comments as properties
- Processing Instructions - capture
<?xml?>declarations and custom PIs - Value Parsing - automatic number, boolean, hex, and scientific notation parsing
- Custom Processors - tag value, attribute value, and tag name transformation callbacks
- Fully Typed - complete TypeScript type definitions
bun install ts-xmlimport { XMLParser } from 'ts-xml'
const parser = new XMLParser()
const result = parser.parse('<root><item>Hello</item></root>')
// { root: { item: 'Hello' } }import { XMLParser } from 'ts-xml'
const parser = new XMLParser({ ignoreAttributes: false })
const result = parser.parse('<book isbn="978-0-123"><title>XML Guide</title></book>')
// { book: { '@_isbn': '978-0-123', title: 'XML Guide' } }import { XMLBuilder } from 'ts-xml'
const builder = new XMLBuilder({ format: true, indentBy: ' ' })
const xml = builder.build({
root: {
item: ['one', 'two', 'three'],
},
})import { XMLValidator } from 'ts-xml'
const result = XMLValidator('<root><child/></root>')
if (result === true) {
console.log('Valid XML')
}
else {
console.log(`Error: ${result.err.msg} at line ${result.err.line}`)
}import { XMLParser, XMLBuilder } from 'ts-xml'
const parser = new XMLParser({ preserveOrder: true })
const ordered = parser.parse('<root><a>1</a><b>2</b><a>3</a></root>')
// Maintains original element order as arrays
const builder = new XMLBuilder({ preserveOrder: true })
const xml = builder.build(ordered) // Round-trips correctly| Option | Type | Default | Description |
|---|---|---|---|
attributeNamePrefix |
string |
"@_" |
Prefix for attribute names |
attributesGroupName |
string | false |
false |
Group attributes under this key |
textNodeName |
string |
"#text" |
Property name for text content |
ignoreAttributes |
boolean |
true |
Skip attribute parsing |
removeNSPrefix |
boolean |
false |
Strip namespace prefixes |
allowBooleanAttributes |
boolean |
false |
Allow attributes without values |
alwaysCreateTextNode |
boolean |
false |
Always create text node property |
trimValues |
boolean |
true |
Trim whitespace from values |
parseTagValue |
boolean |
true |
Parse numbers/booleans from text |
parseAttributeValue |
boolean |
false |
Parse numbers/booleans from attributes |
processEntities |
boolean |
true |
Decode XML entities |
htmlEntities |
boolean |
false |
Decode HTML entities |
commentPropName |
string | false |
false |
Property name for comments |
cdataPropName |
string | false |
false |
Property name for CDATA sections |
piPropName |
string | false |
false |
Property name for processing instructions |
preserveOrder |
boolean |
false |
Maintain element ordering |
stopNodes |
string[] |
[] |
Tags whose content is not parsed |
unpairedTags |
string[] |
[] |
Tags that don't need closing |
numberParseOptions |
NumberParseOptions |
{ hex: true, leadingZeros: true, scientific: true } |
Number parsing behavior |
| Option | Type | Default | Description |
|---|---|---|---|
attributeNamePrefix |
string |
"@_" |
Prefix for attribute names |
textNodeName |
string |
"#text" |
Property name for text content |
ignoreAttributes |
boolean |
false |
Skip attributes when building |
format |
boolean |
false |
Pretty-print output |
indentBy |
string |
" " |
Indentation string |
suppressEmptyNode |
boolean |
false |
Render empty nodes as self-closing |
suppressBooleanAttributes |
boolean |
true |
Render boolean attributes without ="true" |
processEntities |
boolean |
true |
Encode entities in output |
preserveOrder |
boolean |
false |
Build from ordered format |
Benchmarked on Apple M3 Pro using mitata, comparing against fast-xml-parser, xml2js, and sax.
| Benchmark | ts-xml | fast-xml-parser | xml2js | sax |
|---|---|---|---|---|
| Simple XML | 344 ns | 1.64 µs (4.8x slower) | 2.03 µs | 885 ns |
| Medium (3 products + attrs) | 6.67 µs | 27.4 µs (4.1x slower) | 18.0 µs | 12.3 µs |
| Large (100 products) | 321 µs | 1.16 ms (3.6x slower) | 2.04 ms | 1.88 ms |
| Very Large (1000 products) | 3.02 ms | 13.0 ms (4.3x slower) | 9.83 ms | 4.90 ms |
| CDATA | 831 ns | 2.89 µs (3.5x slower) | 5.62 µs | 5.36 µs |
| Deep nesting (50 levels) | 9.33 µs | 59.1 µs (6.3x slower) | 43.0 µs | 27.5 µs |
| RSS Feed | 8.33 µs | 40.4 µs (4.9x slower) | 53.9 µs | 28.7 µs |
| Entities | 2.28 µs | 6.05 µs (2.7x slower) | 9.42 µs | 6.42 µs |
| Namespaces | 3.42 µs | 14.4 µs (4.2x slower) | 11.6 µs | 7.91 µs |
| Benchmark | ts-xml | fast-xml-parser | xml2js |
|---|---|---|---|
| Small object | 1.61 µs | 3.83 µs (2.4x slower) | 8.60 µs |
| Large (100 products) | 42.6 µs | 118 µs (2.8x slower) | 197 µs |
| Formatted output | 1.67 µs | 4.07 µs (2.4x slower) | 8.05 µs |
| Benchmark | ts-xml | fast-xml-parser |
|---|---|---|
| Valid XML | 3.30 µs | 7.79 µs (2.4x slower) |
| Large valid (1000 products) | 1.39 ms | 3.37 ms (2.4x slower) |
| Invalid XML (early exit) | 260 ns | 591 ns (2.3x slower) |
| Benchmark | ts-xml | fast-xml-parser |
|---|---|---|
| Parse + Build (medium) | 9.15 µs | 34.7 µs (3.8x slower) |
Run benchmarks yourself:
bun run bench
bun test724 tests across 6 test files covering parser, builder, validator, entities, and edge cases.
Please see our releases page for more information on what has changed recently.
Please see CONTRIBUTING for details.
For help, discussion about best practices, or any other conversation that would benefit from being searchable:
For casual chit-chat with others using this package:
Join the Stacks Discord Server
"Software that is free, but hopes for a postcard." We love receiving postcards from around the world showing where Stacks is being used! We showcase them on our website too.
Our address: Stacks.js, 12665 Village Ln #2306, Playa Vista, CA 90094, United States 🌎
We would like to extend our thanks to the following sponsors for funding Stacks development. If you are interested in becoming a sponsor, please reach out to us.
The MIT License (MIT). Please see LICENSE for more information.
Made with 💙
