New in Bun v1.4
- Parse and stringify XML with
Bun.XML.parseandBun.XML.stringify import&requireXML files as modules at runtime (including hot reloading & watch mode support)import&requireXML files in frontend apps with Bun’s bundler
Runtime API
Bun.XML.parse()
Parse an XML document into a plain JavaScript object.
@attr / #text convention most XML-to-object libraries use. It works like this:
- The result has one key, the root element’s name.
- An element with no attributes and no child elements becomes its character data: a string,
""when empty.<paid/>and<paid></paid>are the same thing in XML. - Any other element becomes an object. It has a
"@name"key per attribute, then one key per distinct child element name and a"#text"key for the element’s own text, in the order each first appears. - When a child element name occurs more than once in the element, its key holds an array in document order. Otherwise it holds the single value. See One or many.
compactchooses a structure; it does not change values. Bun returns text as written: leading, trailing and internal whitespace included, CDATA sections and entity references expanded, line ends normalized to\n— the same text the tree shape gives for that element. Because an element has one"#text", Bun concatenates its text runs and leaves out whitespace-only runs that sit between child elements (the document’s layout). If your documents are hand-formatted (<name>\n value\n</name>), trim where you read.- All values are strings. Nothing is coerced to numbers, booleans, or
null. - Names are kept as written, namespace prefix included (
"soap:Body");xmlnsdeclarations are ordinary attributes. - Comments, processing instructions, the
<?xml …?>declaration and the<!DOCTYPE …>are not represented.
@ and # cannot start an XML name, so attribute and text keys do not collide with child element keys.
The compact shape is for data. It does not keep the relative order of differently named siblings, or where text sat relative to child elements:
{ compact: false } to get the root element as a tree that keeps the element’s content in document order:
{ name, attributes, children }; both keys are present even when empty. children holds the element’s content in order: text as strings (as written, whitespace-only runs included, adjacent text merged), child elements, comments as { comment }, and processing instructions as { target, data }. Tell object children apart by which key they have. As in the compact shape, the tree does not represent the declaration, the DOCTYPE, or anything before or after the root element.
One or many
In the compact shape a list of one and a list of two have different types (entry: {…} vs entry: [{…}, {…}]). An element that is usually a string also becomes an object when it carries an attribute (<title> vs <title type="html">). Read values that you iterate, or that may carry attributes, defensively:
children is an array and each element is an object.
Input types and encodings
XML.parse accepts a string, or bytes as a Buffer, TypedArray, DataView, ArrayBuffer, or Blob.
A string is already-decoded text, so its encoding declaration is checked for syntax but otherwise ignored. Bytes are decoded per the XML rules: a byte-order mark or the encoding in <?xml version="1.0" encoding="..."?> selects UTF-8 (the default), UTF-16 (either byte order), or ISO-8859-1. Other encodings throw.
Error handling
Bun.XML.parse() throws a SyntaxError when the document is not well-formed (there is no lenient mode), and a RangeError for pathologically deep nesting:
Bun.XML.stringify()
Serialize one element, in either shape, to XML.
name and a children or attributes property. Inside children, an object with name is an element, one with comment is a comment, and one with target is a processing instruction. Anything else is a compact object with one key naming the root element. Bun writes keys in order, @-keys as attributes. Strings, numbers, booleans and bigints become text via String(), and a Date becomes its ISO string. null becomes an empty element. Bun skips undefined, functions and symbols, as JSON.stringify does. An array is one element per item.
The output is well-formed XML, or stringify throws. Bun escapes &, < and >. It writes ", tabs and newlines in attribute values, and carriage returns anywhere, as character references so they parse back unchanged. Values XML cannot hold produce an error rather than a broken document: element and attribute names that are not XML names ("first name", "0"), characters outside XML’s repertoire (U+0000 and other control characters, unpaired surrogates — XML 1.0 has no escape for these), -- inside a comment, ?> inside a processing instruction, an array at the root or inside another array, and circular structures.
The result is the element only, with no <?xml …?> declaration and no DOCTYPE, so you can concatenate results inside an enclosing element. To write a file, prepend the prolog yourself:
Pretty printing
Pass aspace argument (a number of spaces or an indent string, as with JSON.stringify) to indent element-only content. Bun writes an element that contains text on one line, so indentation does not change character data:
null or undefined.
For a value that XML.parse produced, in either shape, XML.parse(XML.stringify(value)) gives back an equal value.
Module Import
ES Modules
You can import XML files directly. Bun decodes the file like bytes passed toXML.parse (UTF-8, UTF-16, or ISO-8859-1 per the byte-order mark or declaration). The module’s value is the compact object described above:
config.xml
Default Import
app.ts
Named Import
The root element is also available as a named import:app.ts
CommonJS
app.ts
Import Attributes
Usewith { type: "xml" } to parse a file with another extension as XML:
Hot Reloading with XML
When you run your application withbun --hot, Bun reloads XML files when they change:
server.ts
terminal
Bundler Integration
When you bundle with Bun, the bundler parses imported XML files at build time and inlines them as JavaScript objects:terminal
- Zero runtime XML parsing overhead in production
- Smaller bundle sizes
- Tree shaking of unused properties
Dynamic Imports
You can import XML files dynamically:Conformance
Bun’s XML parser is written in Rust and implements XML 1.0 (Fifth Edition) as a non-validating processor that does not read external entities:- The whole document, including the internal DTD subset, must be well-formed — anything else throws a
SyntaxError. - The parser expands internal entities declared in the document, with expansion limits so “billion laughs” payloads fail instead of exhausting memory. It normalizes attribute values and applies attribute defaults declared in the internal subset.
- The parser does not fetch or read external DTDs or external entities, so there is no XXE surface. In a document with no DTD, a reference to an undeclared entity is an error. When the DOCTYPE points at an external subset (or uses parameter entities) that could have declared the entity, the parser keeps the reference as written (
stays ), unless the document saysstandalone="yes". - The parser validates nothing against the DTD. It does not resolve namespaces and keeps prefixed names verbatim.
Performance
The parser works in two stages, like Bun’s JSON parser. A SIMD pass (runtime-dispatched AVX2/AVX-512/NEON/SVE kernels) finds the bytes that can change the parse, so the parser never scans character data, attribute values, comments and CDATA sections a byte at a time. Element and attribute names reuse JavaScriptCore’s atom-string cache the same wayJSON.parse does.
bench/xml/xml.mjs compares Bun.XML.parse with popular npm parsers on the same documents (lower is better; Linux x64, one core):