Convert XML into JavaScript object for Node.js

Source code to load an XML file into a JavaScript object, or vice versa, saving an object to an XML file.

In an HTML page, we use the DOMParser Object to convert XML, and XMLHttpRequest to load the document. But there are browser objects, they are not available on Node.js. To replace them, we will use the Sax.js module that loads XML tags one by one and converts them into basic JS objects, and a specific code to assemble these components into a single structured object.

This code is part of the Scriptol-JavaScript compiler runtime since version 1.4.

Tag names and XML attributes become the names of properties of an object. In the case of an attribute a value is assigned to this property. In the case of a tag, the XML element in its whole is assigned.

Example:

<Car speed="100" brand="Ferrari">
   <passengers>Alpha, Beta, Delta</passengers>
</Car>

The JS object is:

{
  Car : {
  "speed": 100,
  "brand": Ferrari,
  "passengers": {
     "data": "Alpha, Beta, Delta"
  }
 }
} 

The content of a tag is assigned by convention to the "data" property.

This is simple but there is still a barrier: If the XML document contains several tags with the same name on the same level, it can not be converted directly to object properties that must be unique. So they are placed in an array and this array is assigned by convention to the "array" property.

Example:

<Car speed="100" brand="Ferrari">
   <passenger>Alpha</passenger>
   <passenger>Beta</passenger>
   <passenger>Delta</passenger>
</Car>

The JS object is:

{ 
 Car: {
  "speed": 100,
  "brand": Ferrari,
  "array" = [ 
     { "passenger" : "Alpha" } ,
     { "passenger" : "Beta" } ,
     { "passenger" : "Delta" } 
    ]
  }
 }
} 

Loading XML into an object (deserialize)

Here is the JavaScript code that loads the XML file into an object. It is suitable for simple documents such as configuration files but does not take into account the CDATA and other complex document elements (XML can be very complicated).

function parseXML(data)
{
  var data = data.toString("utf8");
  var sax = require("sax");
  
  var parser = sax.parser(true, { trim:true });
  parser.onerror = function (e) {
    console.log("XML error: ", e.toString());
    return{};
  };

  var ctag = null;
  var xmlroot = null;
  
  parser.ontext = function (t) {
      if (ctag && t.length > 0) { 
          ctag["data"] = t;
      }   
  }    
  
  parser.onopentag = function (node) {
    var name = node.name;
    var parent = ctag;
    ctag = {};
    ctag.array = [];
    ctag.idFlag = false;   // same tags at same level
    if (xmlroot === null) {
      xmlroot = {};
      xmlroot[name] = ctag;
    }
    else
    {
      ctag.parent = parent;
      var xtag = {};
      xtag[name]= ctag;
      parent.array.push(xtag);
    }
    
    for(var k in node.attributes) {
      ctag[k] = node.attributes[k];
    }

    while(parent && !parent.idFlag) 
    {
        for(var i=0; i < parent.array.length - 1; i++) 
        {
           var elem = parent.array[i];
           for(var key in elem) 
           {
            if(key == name) parent.idFlag=true;
            break;
           }
        }
        break;
    }  
  };

  parser.onclosetag = function(name) {
    if(ctag.idFlag == false) // only one child / all childs different
    {
        for(var i = 0; i < ctag.array.length; i++) {
          var xtag = ctag.array[i];
          for(var u in xtag) {
              ctag[u]=xtag[u];
          } 
        }
        delete ctag.array;
     }
    delete ctag.idFlag;
    if (ctag.parent) {
        var parent = ctag.parent;
        delete ctag.parent;
        ctag = parent;
    }
  }

  parser.write(data).end();
  return xmlroot;
}

var filename="test.xml";
var a = fs.readFileSync(filename).toString();
var obj = parseXML(a);

Assign to filename any XML file.

Accessing the content

To access the individual tags, scriptol runtime offers the getById () function.

function getById(d, idval) {
for(var k in d) {
if(typeof d[k] === "object") {
var dsub = d[k];
if("id" in dsub && dsub.id == idval) return d;
var dret = getById(dsub, idval)
if(dret !== false) return dret;
}
}
return false
}

The function takes into account the problem of identical tags.

Saving a JavaScript object into an XML file (serialize)

To update the file once modified in the JavaScript program, we have not to convert properties and nested objects into tags and attributes.

The value of a property "data" becomes the content of a tag, and the elements of an "array" property each become a tag.

var XMLStorage = "";
function xmlSub(d, name) 
{
  var flag = true;
  if(name=='array') {
    for(var i = 0; i < d.length; i++)
    {
        var tag = d[i];
        var o;
        for(var k in tag) { o = tag[k]; break; }
        XMLStorage += "<" + k;
        flag = xmlSub(o, k);
        XMLStorage += "\n";
      flag = false;      
      continue;
    }
    if (x == "data") { 
      XMLStorage += ">" + d[x];
      flag = false;
    }  
    else { 
      XMLStorage += " " + x + "=\""+ d[x] + "\"";
      flag = true;
    }    
  }
  return flag;  
}

function saveXML(d, filename) 
{
  XMLStorage = '<?xml version="1.0" encoding="UTF-8"?>';
  if(xmlSub(d)) XMLStorage += ">\n";
  fs.writeFileSync(filename, XMLStorage);
}

The full code with a demo is available for download.

To make it work, you must install Node.js and then the sax module with this command:

npm install sax

After you download and extract the archive, go to xml-js and type:

node xml-js.js