Convert JSON to XML: Mapping Rules, Namespaces & Node.js Libraries

Last updated:

Converting JSON to XML requires a mapping convention — the two most common are BadgerFish (JSON $ key maps to XML text content, @ prefix maps to XML attributes) and the GData convention (arrays become repeated sibling elements with the same tag name). JSON has no native concept of XML attributes, namespaces, processing instructions, or mixed content — a user: { name: "Alice", age: 30 } JSON object can map to <user><name>Alice</name><age>30</age></user> or <user name="Alice" age="30"/>, requiring an explicit convention choice before converting. This guide covers JSON-to-XML mapping conventions, handling arrays and nested objects, XML attributes and namespaces in JSON, the xml2js and fast-xml-parser Node.js libraries, and XSLT transformation of XML derived from JSON.

JSON-to-XML Mapping Conventions: BadgerFish and GData

Every JSON-to-XML converter must decide how to represent XML concepts that have no JSON equivalent: attributes, text nodes mixed with child elements, namespaces, and processing instructions. Four conventions dominate — BadgerFish, GData/Google, xml2js default, and Parker — and choosing the wrong one for your use case causes silent data loss or round-trip failures.

// Source XML
// <user id="1" role="admin">
//   <name>Alice</name>
//   <score>95.5</score>
// </user>

// ── BadgerFish convention ──────────────────────────────────────
// @ prefix → XML attributes
// $ key    → XML text content
// Best for: full round-trip fidelity, mixed content
{
  "user": {
    "@id": "1",
    "@role": "admin",
    "name": { "$": "Alice" },
    "score": { "$": "95.5" }
  }
}

// ── GData / Google convention ──────────────────────────────────
// $t key   → XML text content
// @ prefix → XML attributes
// Arrays of same-named elements become JSON arrays
{
  "user": {
    "@id": "1",
    "@role": "admin",
    "name": { "$t": "Alice" },
    "score": { "$t": "95.5" }
  }
}

// ── xml2js default convention ──────────────────────────────────
// _ key    → XML text content (charkey option)
// $ key    → XML attributes object (attrkey option)
// Best for: Node.js projects using the xml2js / soap ecosystem
{
  "user": {
    "$": { "id": "1", "role": "admin" },
    "name": [ "Alice" ],
    "score": [ "95.5" ]
  }
}

// ── Parker convention (simplest) ──────────────────────────────
// Element text → direct string value (no wrapping object)
// Attributes are DROPPED — use only for read-only XML consumption
{
  "user": {
    "name": "Alice",
    "score": "95.5"
  }
}

// ── Choosing a convention ──────────────────────────────────────
// Need round-trip fidelity (XML → JSON → XML)?  →  BadgerFish
// Using xml2js / soap npm packages?              →  xml2js default
// New project, high performance?                 →  fast-xml-parser (@_ prefix)
// Read-only consumption, simplest JSON shape?    →  Parker

Convention interoperability is the biggest source of bugs in JSON-XML pipelines: if you parse XML with xml2js (which produces $ for attributes) and then pass the output to a BadgerFish builder (which expects @ for attributes), all attributes are silently dropped. Always use the same library for both parsing and building in a round-trip pipeline. The xml2js attrkey and charkey options let you customize the special key names if you need to match a different convention.

Handling Arrays and Nested Objects in XML

JSON arrays map to repeated sibling XML elements with the same tag name, and nested JSON objects map to child XML elements. The most common parsing pitfall is the "array of one" problem: a single child element parses as a string unless you configure explicitArray: true. Getting array handling right is critical for schema-correct XML generation.

// ── JSON arrays → repeated XML sibling elements ───────────────
// JSON input
{
  "library": {
    "book": [
      { "title": "Clean Code", "author": "Martin" },
      { "title": "Refactoring", "author": "Fowler" },
      { "title": "SICP",       "author": "Abelson" }
    ]
  }
}

// XML output (fast-xml-parser XMLBuilder)
// <library>
//   <book><title>Clean Code</title><author>Martin</author></book>
//   <book><title>Refactoring</title><author>Fowler</author></book>
//   <book><title>SICP</title><author>Abelson</author></book>
// </library>

// ── "Array of one" problem ─────────────────────────────────────
// XML with ONE <book> element:
// <library><book><title>Clean Code</title></book></library>

// xml2js default (explicitArray: false) — parses as object, NOT array
// { library: { book: { title: "Clean Code" } } }  ← WRONG: not an array

// xml2js with explicitArray: true — always an array
// { library: { book: [ { title: ["Clean Code"] } ] } }  ← CORRECT

import { parseStringPromise } from 'xml2js'

const result = await parseStringPromise(xmlString, {
  explicitArray: true,   // always wrap children in arrays
  mergeAttrs: false,     // keep attributes in $ key
})

// fast-xml-parser — force specific tags to always be arrays
import { XMLParser } from 'fast-xml-parser'

const parser = new XMLParser({
  isArray: (tagName) => ['book', 'user', 'item'].includes(tagName),
})
const json = parser.parse(xmlString)

// ── Nested objects → child elements ───────────────────────────
// JSON input
{
  "order": {
    "id": "o1",
    "customer": {
      "id": "c1",
      "name": "Alice",
      "address": {
        "street": "123 Main St",
        "city": "Springfield"
      }
    },
    "items": [
      { "sku": "A1", "qty": 2 },
      { "sku": "B3", "qty": 1 }
    ]
  }
}

// XML output
// <order>
//   <id>o1</id>
//   <customer>
//     <id>c1</id>
//     <name>Alice</name>
//     <address>
//       <street>123 Main St</street>
//       <city>Springfield</city>
//     </address>
//   </customer>
//   <items><sku>A1</sku><qty>2</qty></items>
//   <items><sku>B3</sku><qty>1</qty></items>
// </order>

// ── Mixed arrays (objects + primitives) ───────────────────────
// XML: <tags><tag>json</tag><tag>xml</tag><tag>api</tag></tags>
// JSON (fast-xml-parser):
{ "tags": { "tag": ["json", "xml", "api"] } }
// Arrays of primitives work identically to arrays of objects

Wrapping arrays in a container element (like <items> wrapping multiple <item> elements) is standard XML practice for clarity and schema validation. In JSON, this maps to a nested object: { "items": { "item": [...] } }. When the container has only one child, the array-of-one problem re-emerges — always configure explicit array mode for production parsers to prevent type mismatches in downstream code.

XML Attributes and Namespaces from JSON

XML attributes and namespaces require explicit convention support — no JSON library handles them identically. Correct attribute and namespace mapping is essential for interoperability with XML schemas, SOAP services, and namespace-aware XML processors.

// ── XML attributes via xml2js ──────────────────────────────────
// Target XML:
// <user id="42" role="admin"><name>Alice</name></user>

import { Builder } from 'xml2js'

const builder = new Builder({ headless: true })
const xml = builder.buildObject({
  user: {
    $: { id: '42', role: 'admin' },  // $ key → XML attributes
    name: 'Alice',
  },
})
// Output: <user id="42" role="admin"><name>Alice</name></user>

// ── XML attributes via fast-xml-parser ────────────────────────
import { XMLBuilder } from 'fast-xml-parser'

const xmlBuilder = new XMLBuilder({
  ignoreAttributes: false,       // must be false to include attributes
  attributeNamePrefix: '@_',     // JSON keys starting with @_ → XML attributes
})
const xml2 = xmlBuilder.build({
  user: {
    '@_id': '42',
    '@_role': 'admin',
    name: 'Alice',
  },
})
// Output: <user id="42" role="admin"><name>Alice</name></user>

// ── XML namespaces ─────────────────────────────────────────────
// Target XML:
// <ns:user xmlns:ns="http://example.com/users" ns:id="42">
//   <ns:name>Alice</ns:name>
// </ns:user>

// Using xml2js Builder (attributes under $):
const xmlWithNs = builder.buildObject({
  'ns:user': {
    $: {
      'xmlns:ns': 'http://example.com/users',
      'ns:id': '42',
    },
    'ns:name': 'Alice',
  },
})

// Using fast-xml-parser XMLBuilder:
const xmlWithNs2 = xmlBuilder.build({
  'ns:user': {
    '@_xmlns:ns': 'http://example.com/users',
    '@_ns:id': '42',
    'ns:name': 'Alice',
  },
})

// ── Stripping namespaces when parsing ─────────────────────────
import { XMLParser } from 'fast-xml-parser'

const strippingParser = new XMLParser({
  ignoreAttributes: false,
  removeNSPrefix: true,    // strip "ns:" prefix from element and attribute names
})
// <ns:user ns:id="42"> → { user: { "@_id": "42" } }

// ── Default namespace (xmlns without prefix) ──────────────────
// <root xmlns="http://default.com"><child>value</child></root>
// xml2js parses xmlns as a regular attribute in $:
// { root: { $: { xmlns: 'http://default.com' }, child: ['value'] } }

Namespace stripping (removeNSPrefix: true in fast-xml-parser) is safe when consuming SOAP or XML APIs where the namespace is implicit from the schema, but must be avoided in round-trip pipelines where the output XML must be namespace-valid. When building namespaced XML, always include the xmlns declaration as an attribute on the root element that introduces the namespace — omitting it produces well-formed but invalid namespaced XML that will fail namespace-aware validators.

xml2js: Bidirectional JSON-XML Conversion

xml2js is the most widely used Node.js library for JSON-XML conversion, with over 20 million weekly npm downloads. Its parseStringPromise function parses XML to JSON and its Builder class converts JSON back to XML, supporting the full range of XML features including attributes, CDATA, namespaces, and processing instructions.

import { parseStringPromise, Builder } from 'xml2js'

// ── Parsing XML → JSON ─────────────────────────────────────────
const xmlInput = `
  <catalog>
    <book id="b1" genre="fiction">
      <title>The Hobbit</title>
      <price currency="USD">12.99</price>
      <tags><tag>fantasy</tag><tag>classic</tag></tags>
    </book>
    <book id="b2" genre="nonfiction">
      <title>Clean Code</title>
      <price currency="USD">35.00</price>
      <tags><tag>programming</tag></tags>
    </book>
  </catalog>
`

// parseStringPromise options reference:
const json = await parseStringPromise(xmlInput, {
  explicitArray: true,     // always wrap children in arrays (recommended)
  explicitRoot: true,      // include root element key in output (default: true)
  attrkey: '$',            // key name for attributes (default: '$')
  charkey: '_',            // key name for text content (default: '_')
  mergeAttrs: false,       // merge attributes into the parent object (default: false)
  trim: true,              // trim leading/trailing whitespace from text nodes
  explicitCharkey: false,  // don't include charkey when no attributes present
  ignoreAttrs: false,      // set true to drop all attributes
})

// Resulting JSON:
// {
//   catalog: {
//     book: [
//       { $: { id: 'b1', genre: 'fiction' },
//         title: ['The Hobbit'],
//         price: [ { _: '12.99', $: { currency: 'USD' } } ],
//         tags: [ { tag: ['fantasy', 'classic'] } ] },
//       { $: { id: 'b2', genre: 'nonfiction' },
//         title: ['Clean Code'],
//         price: [ { _: '35.00', $: { currency: 'USD' } } ],
//         tags: [ { tag: ['programming'] } ] }
//     ]
//   }
// }

// ── Building JSON → XML ────────────────────────────────────────
const builder = new Builder({
  headless: false,        // include XML declaration (<?xml version="1.0"?>)
  renderOpts: {
    pretty: true,         // format with indentation
    indent: '  ',
    newline: '\n',
  },
  attrkey: '$',           // must match the attrkey used during parsing
  charkey: '_',
  rootName: 'catalog',   // wrap output in this root element (default: 'root')
  cdata: false,           // set true to wrap text in CDATA sections
})

const xmlOutput = builder.buildObject({
  book: [
    { $: { id: 'b1', genre: 'fiction' }, title: 'The Hobbit' },
    { $: { id: 'b2', genre: 'nonfiction' }, title: 'Clean Code' },
  ],
})

// ── CDATA handling ─────────────────────────────────────────────
// XML with CDATA: <description><![CDATA[<b>HTML</b> content]]></description>
// xml2js parses CDATA as regular text by default
// To produce CDATA in output: set cdata: true in Builder options
// or use explicit CDATA key in input: { description: { _cdata: '<b>HTML</b>' } }

// ── Round-trip fidelity check ──────────────────────────────────
// Parse → build → parse again and compare for attribute preservation
async function roundTrip(xml: string) {
  const json = await parseStringPromise(xml, { explicitArray: true })
  const xmlOut = new Builder().buildObject(json)
  const json2 = await parseStringPromise(xmlOut, { explicitArray: true })
  return JSON.stringify(json) === JSON.stringify(json2)
}

The most common xml2js mistake is using explicitArray: false (which was the old default) and then writing code that assumes arrays — this breaks silently when an element has exactly one child. Always use explicitArray: true in production code and write all field accesses as result.book[0].title[0]. The mergeAttrs: true option merges the $ attribute object into the parent, producing a flatter JSON shape at the cost of potential key collisions between attributes and child elements.

fast-xml-parser: High-Performance XML Parsing

fast-xml-parser is a high-performance XML parser and builder for Node.js and browsers, parsing 10 MB XML files in ~50 ms with a synchronous API and native TypeScript support. Its XMLParser and XMLBuilder classes replace xml2js for new projects requiring performance or TypeScript integration.

import { XMLParser, XMLBuilder, XMLValidator } from 'fast-xml-parser'

// ── Parsing XML → JSON ─────────────────────────────────────────
const parser = new XMLParser({
  ignoreAttributes: false,       // include attributes (default: true = ignore!)
  attributeNamePrefix: '@_',     // prefix for attribute keys in JSON output
  textNodeName: '#text',         // key name for text content in mixed elements
  isArray: (tagName) =>          // always parse these tags as arrays
    ['book', 'item', 'tag', 'user'].includes(tagName),
  parseAttributeValue: true,     // coerce attribute values to numbers/booleans
  parseTagValue: true,           // coerce element text to numbers/booleans
  trimValues: true,              // trim whitespace from text nodes
  cdataPropName: '__cdata',      // key name for CDATA sections
  removeNSPrefix: false,         // set true to strip namespace prefixes
  allowBooleanAttributes: true,  // support <input disabled> style attributes
})

const xmlInput = `<?xml version="1.0"?>
<catalog version="2.0">
  <book id="1" inStock="true">
    <title>Clean Code</title>
    <price>35.00</price>
  </book>
  <book id="2" inStock="false">
    <title>Refactoring</title>
    <price>42.00</price>
  </book>
</catalog>`

const json = parser.parse(xmlInput)
// {
//   catalog: {
//     "@_version": "2.0",
//     book: [
//       { "@_id": 1, "@_inStock": true, title: "Clean Code",   price: 35.00 },
//       { "@_id": 2, "@_inStock": false, title: "Refactoring", price: 42.00 }
//     ]
//   }
// }

// ── Building JSON → XML ────────────────────────────────────────
const builder = new XMLBuilder({
  ignoreAttributes: false,
  attributeNamePrefix: '@_',
  format: true,              // pretty-print with indentation
  indentBy: '  ',
  suppressEmptyNode: true,   // <tag></tag> → <tag/> for empty elements
  cdataPropName: '__cdata',
})

const xmlOutput = builder.build({
  '?xml': { '@_version': '1.0', '@_encoding': 'UTF-8' },
  catalog: {
    '@_version': '2.0',
    book: [
      { '@_id': 1, '@_inStock': true,  title: 'Clean Code',   price: 35.00 },
      { '@_id': 2, '@_inStock': false, title: 'Refactoring', price: 42.00 },
    ],
  },
})

// ── Validation before parsing ──────────────────────────────────
const validationResult = XMLValidator.validate(xmlInput)
if (validationResult !== true) {
  console.error('Invalid XML:', validationResult.err.msg)
}

// ── Parsing large XML files (10 MB+) ──────────────────────────
import { readFileSync } from 'fs'

const largeXml = readFileSync('large-feed.xml', 'utf8')
console.time('parse')
const result = new XMLParser({ ignoreAttributes: false }).parse(largeXml)
console.timeEnd('parse')  // ~50 ms for 10 MB, vs ~150 ms for xml2js

// ── TypeScript typed output ────────────────────────────────────
interface CatalogJSON {
  catalog: {
    '@_version': string
    book: Array<{
      '@_id': number
      '@_inStock': boolean
      title: string
      price: number
    }>
  }
}

const typedResult = parser.parse(xmlInput) as CatalogJSON

A critical fast-xml-parser gotcha: ignoreAttributes defaults to true, meaning attributes are silently dropped unless you explicitly set it to false. This is the opposite of xml2js behavior and the most common source of data loss when switching libraries. Always set ignoreAttributes: false when working with XML that has attributes. The parseTagValue: true option automatically converts numeric and boolean text content to JavaScript primitives, which is convenient but can cause issues if element text looks like a number but should remain a string (e.g., phone numbers with leading zeros — configure parseTagValue: false in those cases).

SOAP XML to JSON: Unwrapping Envelopes

SOAP web services return XML envelopes that must be unwrapped to extract the JSON-serializable response payload. The soap npm package automates this for WSDL-based services, while manual parsing with fast-xml-parser or xml2js is required for services without a WSDL or for custom fault handling.

// ── SOAP envelope structure ────────────────────────────────────
// A SOAP 1.1 response looks like this:
// <?xml version="1.0"?>
// <soap:Envelope
//   xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
//   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
//   <soap:Header>
//     <auth:Token xmlns:auth="http://example.com/auth">abc123</auth:Token>
//   </soap:Header>
//   <soap:Body>
//     <GetUserResponse xmlns="http://example.com/users">
//       <User><Id>42</Id><Name>Alice</Name><Email>alice@example.com</Email></User>
//     </GetUserResponse>
//   </soap:Body>
// </soap:Envelope>

// ── Option 1: soap npm package (auto-converts via WSDL) ────────
import soap from 'soap'

const client = await soap.createClientAsync('http://example.com/service?wsdl')

// Auto-parses SOAP XML response → JavaScript object
const [result] = await client.GetUserAsync({ userId: 42 })
// result = { User: { Id: 42, Name: 'Alice', Email: 'alice@example.com' } }
// No manual XML parsing needed — soap package handles the envelope

// ── Option 2: Manual parsing with fast-xml-parser ─────────────
import { XMLParser } from 'fast-xml-parser'

async function parseSoapResponse(soapXml: string) {
  const parser = new XMLParser({
    ignoreAttributes: false,
    removeNSPrefix: true,   // strip soap:, xsi:, etc. prefixes
    isArray: () => false,   // SOAP responses rarely use arrays at envelope level
  })

  const parsed = parser.parse(soapXml)

  // Navigate: Envelope.Body → response object
  const envelope = parsed['Envelope'] ?? parsed['soap:Envelope']
  const body = envelope?.['Body'] ?? envelope?.['soap:Body']

  if (!body) throw new Error('No SOAP Body found in response')

  // Check for SOAP Fault
  const fault = body['Fault'] ?? body['soap:Fault']
  if (fault) {
    throw new Error(`SOAP Fault: ${fault.faultstring ?? fault.Reason?.Text ?? 'Unknown fault'}`)
  }

  // Return the first child of Body (the actual response element)
  const responseKey = Object.keys(body)[0]
  return body[responseKey]
}

// ── Option 3: xml2js for SOAP with namespace awareness ─────────
import { parseStringPromise } from 'xml2js'

async function parseSoapXml2js(soapXml: string) {
  const result = await parseStringPromise(soapXml, {
    explicitArray: false,
    ignoreAttrs: true,     // SOAP attribute noise is usually not needed
    tagNameProcessors: [(name: string) => name.replace(/^[^:]+:/, '')], // strip ns prefix
  })

  // Navigate to Body content
  const body = result?.Envelope?.Body
  if (!body) throw new Error('No SOAP Body')

  const faultKey = Object.keys(body).find(k => k.includes('Fault'))
  if (faultKey) throw new Error(`SOAP Fault: ${body[faultKey].faultstring}`)

  const responseKey = Object.keys(body)[0]
  return body[responseKey]
}

// ── SOAP Fault as JSON error ───────────────────────────────────
// SOAP 1.1 Fault:
// <soap:Fault>
//   <faultcode>soap:Server</faultcode>
//   <faultstring>User not found</faultstring>
//   <detail><errorCode>404</errorCode></detail>
// </soap:Fault>

// After stripping namespace prefixes and parsing:
// { faultcode: 'Server', faultstring: 'User not found', detail: { errorCode: '404' } }

The soap npm package's WSDL-based client is the most reliable option for SOAP services because it uses the service's own type definitions to parse and validate the response — type coercion, namespace handling, and fault detection are all automatic. For services without a WSDL or for one-off XML responses, the fast-xml-parser approach with removeNSPrefix: true is the most pragmatic: it strips all namespace prefixes and lets you navigate the response by element name without worrying about namespace URIs.

XSLT Transformation and JSON Integration

XSLT (XSL Transformations) enables declarative XML-to-XML and, in XSLT 3.0/XPath 3.1, XML-to-JSON transformation. Saxon-JS brings XSLT 3.0 to Node.js, and the fn:json-to-xml() XPath 3.1 function converts JSON strings directly to XML within an XSLT stylesheet.

// ── XSLT 1.0: XML → XML transformation (server-side with xsltproc) ─
// xsltproc is available on Linux/macOS: apt install xsltproc / brew install libxslt
// xsltproc transform.xsl input.xml > output.xml

// transform.xsl — XSLT 1.0 stylesheet to flatten a SOAP response
// <?xml version="1.0"?>
// <xsl:stylesheet version="1.0"
//   xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
//   xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
//   <xsl:output method="xml" indent="yes"/>
//   <xsl:template match="/">
//     <!-- Copy the first child of soap:Body, stripping the envelope -->
//     <xsl:copy-of select="soap:Envelope/soap:Body/*[1]"/>
//   </xsl:template>
// </xsl:stylesheet>

// ── Saxon-JS: XSLT 3.0 in Node.js ────────────────────────────
// npm install saxon-js xslt3
import SaxonJS from 'saxon-js'

// Compile XSLT stylesheet to .sef.json (Saxon Export Format)
// xslt3 -xsl:transform.xsl -export:transform.sef.json -nogo

// Apply compiled stylesheet to XML
const result = await SaxonJS.transform({
  stylesheetFileName: 'transform.sef.json',
  sourceText: xmlString,
  destination: 'serialized',
}, 'async')
// result.principalResult → transformed XML string

// ── XSLT 3.0: XML → JSON output ──────────────────────────────
// XSLT 3.0 stylesheet with JSON output method
// <xsl:stylesheet version="3.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
//   <xsl:output method="json" indent="yes"/>
//   <xsl:template match="/">
//     <xsl:map>
//       <xsl:map-entry key="'users'">
//         <xsl:sequence select="array { /users/user/map { 'id': @id, 'name': name } }"/>
//       </xsl:map-entry>
//     </xsl:map>
//   </xsl:template>
// </xsl:stylesheet>

// ── fn:json-to-xml(): JSON string → XML (XPath 3.1) ──────────
// fn:json-to-xml() converts a JSON string to the XPath 3.1 XML representation
// Input JSON: '{"name":"Alice","scores":[95,87]}'
// Output XML:
// <map xmlns="http://www.w3.org/2005/xpath-functions">
//   <string key="name">Alice</string>
//   <array key="scores">
//     <number>95</number>
//     <number>87</number>
//   </array>
// </map>

// Use in a stylesheet to process JSON data with XSLT:
// <xsl:variable name="parsed" select="fn:json-to-xml($jsonString)"/>
// <xsl:for-each select="$parsed/map/array[@key='scores']/number">
//   <score><xsl:value-of select="."/></score>
// </xsl:for-each>

// ── xsltproc pipeline for server-side XML → JSON ──────────────
import { execFile } from 'child_process'
import { promisify } from 'util'

const execFileAsync = promisify(execFile)

async function transformXml(xslPath: string, xmlPath: string): Promise<string> {
  const { stdout } = await execFileAsync('xsltproc', [xslPath, xmlPath])
  return stdout
}

// Then parse the transformed XML with fast-xml-parser to get final JSON

XSLT 3.0's fn:json-to-xml() function produces a canonical XML representation of JSON defined by the W3C XPath 3.1 specification, using <map>, <array>, <string>, and <number> elements in the http://www.w3.org/2005/xpath-functions namespace. This representation is verbose but fully round-trippable via the companion fn:xml-to-json() function. For production Node.js pipelines requiring XSLT, Saxon-JS is the standard choice — it supports XSLT 3.0, XPath 3.1, and produces both XML and JSON output, while xsltproc is limited to XSLT 1.0 and XML output only.

Key Terms

BadgerFish convention
A JSON-XML mapping convention developed by David Lee that uses special key prefixes to represent XML concepts in JSON. The rules: XML element text content is stored under the $ key; XML attributes are prefixed with @; XML namespace declarations (xmlns:prefix) are represented as @-prefixed attributes; arrays of sibling elements with the same tag name become JSON arrays. BadgerFish enables full round-trip fidelity — you can convert XML to JSON and back to identical XML — making it suitable for XML processing pipelines where the JSON representation is an intermediate format. It is the most widely implemented convention across different programming languages and is the basis for several XML-to-JSON specification proposals.
XML namespace
A mechanism for disambiguating XML element and attribute names across documents from different vocabularies. A namespace is identified by a URI (not necessarily a real URL) and declared with an xmlns:prefix attribute on any element, making the prefix available for that element and all its descendants. Elements and attributes using the namespace are prefixed: <ns:user xmlns:ns="http://example.com">. The default namespace (xmlns="...") applies to unprefixed elements within its scope. In JSON-XML conversion, namespace declarations are typically represented as attributes (under the $ key in xml2js or with the @_ prefix in fast-xml-parser). The removeNSPrefix: true option in fast-xml-parser strips prefixes from element names in JSON output, simplifying the structure at the cost of losing namespace URI information.
CDATA
Character Data — an XML construct that marks a text section where XML special characters (<, >, &) are treated as literal text rather than markup. CDATA sections have the syntax <![CDATA[...text content...]]> and are commonly used in XML to embed HTML, JavaScript, or other content that contains XML-reserved characters without requiring escaping. When parsing XML with xml2js or fast-xml-parser, CDATA sections are decoded to their text content by default. When building XML from JSON with CDATA sections, configure cdata: true in the xml2js Builder or cdataPropName: '__cdata' in fast-xml-parser and use the configured key to mark text values that should be wrapped in CDATA in the output.
SOAP envelope
The outermost XML element in a SOAP (Simple Object Access Protocol) message, defined in the http://schemas.xmlsoap.org/soap/envelope/ (SOAP 1.1) or http://www.w3.org/2003/05/soap-envelope (SOAP 1.2) namespace. Every SOAP message is a <soap:Envelope> containing an optional <soap:Header> (for metadata like authentication tokens) and a required <soap:Body> (containing the actual request or response payload). To convert a SOAP response to JSON, you must unwrap the envelope by navigating to soap:Envelope/soap:Body and extracting the first child element. SOAP faults (errors) appear as <soap:Fault> elements inside the Body and must be checked before processing the response as data.
XSLT
XSL Transformations — a declarative, functional programming language for transforming XML documents into other XML documents, HTML, plain text, or (in XSLT 3.0) JSON. XSLT stylesheets are themselves XML documents that use <xsl:template match="..."> rules to pattern-match nodes in the source document and produce output. XSLT 1.0 is widely supported (including by the xsltproc command-line tool on Linux/macOS) and handles XML-to-XML transformations. XSLT 3.0 (supported by Saxon) adds method="json" output, the fn:json-to-xml() and fn:xml-to-json() XPath 3.1 functions, and streaming for large documents. XSLT is the standard tool for XML normalization, envelope stripping, schema migration, and generating HTML from XML data feeds.
round-trip fidelity
The property of a JSON-XML conversion where converting XML to JSON and then back to XML produces an output structurally identical to the original. Full round-trip fidelity requires that all XML features are preserved in the JSON representation: element names, text content, attributes, namespace declarations, and element ordering. The BadgerFish convention achieves full round-trip fidelity. The Parker convention does not — it drops all attribute information and thus cannot reconstruct the original XML from the JSON. The xml2js default convention achieves fidelity for most documents but may lose XML declaration details (processing instructions, DTD references) and comment nodes, which are not included in the JSON output by default. Round-trip fidelity is essential for XML-processing pipelines where JSON is an intermediate format, and irrelevant for applications that only consume XML data without needing to regenerate XML.

FAQ

How do I convert JSON to XML in JavaScript?

To convert JSON to XML in JavaScript (Node.js), use fast-xml-parser's XMLBuilder or xml2js's Builder. With fast-xml-parser: npm install fast-xml-parser, then new XMLBuilder({ ignoreAttributes: false, attributeNamePrefix: "@_" }) and call builder.build(jsonObject). With xml2js: npm install xml2js, then new Builder() and builder.buildObject(jsonObject) — attributes go in the $ key of each object. For a browser-only solution without a library, write a recursive function: for each key in the object, wrap the value in <key>...</key> tags, recurse for nested objects, and repeat the element tag for array items. fast-xml-parser is the recommended choice for new projects due to its synchronous API, TypeScript support, and ~3x faster parsing speed compared to xml2js.

What is the BadgerFish convention for JSON-XML conversion?

BadgerFish is a JSON-XML mapping convention that uses special key prefixes to represent XML concepts without JSON equivalents. Rules: (1) element text content goes under the $ key — <name>Alice</name> becomes { "name": { "$": "Alice" } }; (2) attributes are prefixed with @<user id="1"> becomes { "user": { "@id": "1" } }; (3) namespace declarations are @-prefixed attributes; (4) sibling elements with the same tag become JSON arrays. BadgerFish supports full round-trip fidelity: converting XML to JSON and back produces identical XML. It is the most widely implemented convention across languages. Use it when you need full attribute and namespace preservation or when building tools that must interoperate across different programming environments with a shared JSON-XML convention.

How do I handle XML attributes when converting to and from JSON?

XML attributes must be mapped to JSON using a library-specific key convention, since JSON has no attribute concept. In xml2js, attributes are stored under the $ key: <user id="1" role="admin"> parses to { "user": { "$": { "id": "1", "role": "admin" } } }, and Builder reads attributes from the same $ key. In fast-xml-parser, set ignoreAttributes: false (critical — it defaults to true and silently drops attributes) and attributes are prefixed with @_ in the output: { "user": { "@_id": "1", "@_role": "admin" } }. Use the same convention (and library) for both parsing and building to avoid data loss. For the BadgerFish convention, prefix attribute keys with @ directly in the JSON object. Never mix conventions — passing xml2js output to a BadgerFish builder silently drops all attributes.

How do I convert XML namespaces to JSON?

XML namespace declarations (xmlns:prefix="uri") are treated as attributes by most JSON-XML libraries. In xml2js, they appear under the $ key alongside other attributes: { "$": { "xmlns:ns": "http://example.com" } }. In fast-xml-parser with ignoreAttributes: false, they appear with the @_ prefix: { "@_xmlns:ns": "http://example.com" }. Prefixed element names (ns:user) become the JSON key literally: { "ns:user": { ... } }. To strip namespace prefixes for simpler JSON, use fast-xml-parser's removeNSPrefix: true option — this converts ns:user to user in the JSON output. Namespace stripping is safe for consuming SOAP or XML APIs where the schema is known, but avoid it in round-trip pipelines because the stripped output cannot reconstruct valid namespaced XML.

What is the difference between xml2js and fast-xml-parser?

xml2js and fast-xml-parser are the two dominant Node.js JSON-XML libraries. xml2js uses SAX-based streaming internally and has a callback/Promise API (parseStringPromise for parsing, Builder for building). It is mature, widely used in legacy codebases, and is the default used by the soap npm package — but it is slow (~150 ms for 10 MB XML) and its default behavior (explicitArray: false, attributes under $) is surprising. fast-xml-parser is a pure JavaScript parser with a synchronous API and native TypeScript types. It parses the same 10 MB XML in ~50 ms (3x faster), has cleaner configuration, and is better suited for new projects. The key behavioral difference: fast-xml-parser defaults to ignoreAttributes: true (dropping all attributes silently), while xml2js includes attributes by default. Both libraries support CDATA, namespaces, and round-trip XML building.

How do I convert a SOAP XML response to JSON?

The easiest path is the soap npm package: npm install soap, create a client with await soap.createClientAsync(wsdlUrl), and call await client.methodAsync(args) — the package auto-parses the SOAP XML envelope and returns a JavaScript object. For manual parsing without a WSDL: use fast-xml-parser with removeNSPrefix: true to parse the XML and strip soap: prefixes, then navigate to parsed.Envelope.Body and extract the first child element (the actual response). Check for a Fault key in the Body before processing as data. For xml2js, use the tagNameProcessors option with a function that strips namespace prefixes. Handle SOAP 1.1 faults via the faultstring field and SOAP 1.2 faults via the Code/Reason structure in the Fault element.

How do I convert JSON arrays to XML?

JSON arrays map to repeated sibling XML elements with the same tag name. In fast-xml-parser XMLBuilder, pass an array value under a key and the builder repeats the element: { items: { item: ["a", "b", "c"] } } produces <items><item>a</item><item>b</item><item>c</item></items>. In xml2js Builder, the same structure works identically. The reverse (XML to JSON) requires careful configuration: with xml2js, set explicitArray: true to always parse child elements as arrays even when there is only one, preventing the "array of one" type inconsistency. With fast-xml-parser, use the isArray callback option to specify which tag names should always be parsed as arrays: isArray: (tagName) => ["item", "user"].includes(tagName). Always configure explicit array mode in production to avoid type errors when the XML has exactly one child element.

Which JSON-to-XML convention should I choose?

Choose based on your use case: (1) Need full round-trip fidelity with mixed content and namespaces? Use BadgerFish ($ for text, @ for attributes). (2) Working with an existing Node.js codebase that uses xml2js or the soap package? Use the xml2js convention ($ for attributes object, _ for text) to stay consistent with the ecosystem. (3) New project requiring high performance and TypeScript? Use fast-xml-parser with attributeNamePrefix: '@_'. (4) Read-only XML consumption where you only care about element text and child structure (no attributes)? Use the Parker convention or set ignoreAttributes: true in fast-xml-parser for the simplest JSON shape. Never mix conventions between parsing and building steps — this silently drops attributes or creates malformed XML. Document your convention choice in a code comment so future maintainers do not accidentally switch parsers without updating the downstream code.

Further reading and primary sources

  • xml2js on npmxml2js API reference, options, and Builder documentation for bidirectional JSON-XML conversion
  • fast-xml-parser DocumentationXMLParser and XMLBuilder API, configuration options, and performance benchmarks
  • BadgerFish ConventionOriginal BadgerFish JSON-XML mapping convention specification by David Lee
  • Saxon-JS: XSLT 3.0 for Node.jsSaxon-JS documentation for running XSLT 3.0 stylesheets in Node.js with JSON output support
  • XPath 3.1: fn:json-to-xml()W3C specification for the fn:json-to-xml() XPath 3.1 function that converts JSON strings to the canonical XML representation