11

Trying to parse XML into JSON with xml2js and then return the JSON to XML using xmlbuilder (usually after modifying the content programmatically).

I think that the two should be complements, per this post https://github.com/oozcitak/xmlbuilder-js/issues/69. But am having some difficulty, and it's gotta be that I'm not getting the config parameters right.

Here's the code I'm running:

var xml = fs.readFileSync(__dirname + '/../xml/theme.xml', 'utf8');

xml2js.parseString(xml, { attrkey: '@',  xmlns: true }, function(err, json) {
    var xml2 = xmlbuilder.create(json,
       {version: '1.0', encoding: 'UTF-8', standalone: true}
    ).end({pretty: true, standalone: true})
});

Here's the first bit of the original XML:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<a:theme xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" name="Office Theme">
    <a:themeElements>
        <a:clrScheme name="Office">
            <a:dk1>
                <a:sysClr val="windowText" lastClr="000000"/>
            </a:dk1>
            <a:lt1>
                <a:sysClr val="window" lastClr="FFFFFF"/>
            </a:lt1>
            <a:dk2>
                <a:srgbClr val="1F497D"/>
            </a:dk2>
            ...
     </a:themeElements>           
 </a:theme>

Here;s how xml2js parses that to JSON, this looks right to me:

{
    "a:theme": {
        "@": {
            "xmlns:a": {
                "name": "xmlns:a",
                "value": "http://schemas.openxmlformats.org/drawingml/2006/main",
                "prefix": "xmlns",
                "local": "a",
                "uri": "http://www.w3.org/2000/xmlns/"
            },
            "name": {
                "name": "name",
                "value": "Office Theme",
                "prefix": "",
                "local": "name",
                "uri": ""
            }
        },
        "@ns": {
            "uri": "http://schemas.openxmlformats.org/drawingml/2006/main",
            "local": "theme"
        },
        "a:themeElements": [
            {
                "@ns": {
                    "uri": "http://schemas.openxmlformats.org/drawingml/2006/main",
                    "local": "themeElements"
                },
                "a:clrScheme": [
                    {
                        "@": {
                            "name": {
                                "name": "name",
                                "value": "Office",
                                "prefix": "",
                                "local": "name",
                                "uri": ""
                            }
                        },
                        "@ns": {
                            "uri": "http://schemas.openxmlformats.org/drawingml/2006/main",
                            "local": "clrScheme"
                        },
                       ...

Note that in the JSON above:

  1. the attribute (e.g. name=) are turned into keys inside an @ object and
  2. the attribute values are turned into objects

Now here's how it looks when xmlbuilder turns it back into XML:

<a:theme ="[object Object]" ns="[object Object]">
  <a:themeElements ns="[object Object]">
    <a:clrScheme ="[object Object]" ns="[object Object]">
      <a:dk1 ns="[object Object]">
        <a:sysClr ="[object Object]" ns="[object Object]"/>
      </a:dk1>
      <a:lt1 ns="[object Object]">
        <a:sysClr ="[object Object]" ns="[object Object]"/>
      </a:lt1>
       ...
    </a:themeElements>
 </a:theme>

So there are two problems that XML builder is facing: * it's not recognizing the attribute names within the @ object and * it's not recognizing the attribute value within the attribute object

Hacking it appears that xmlbuilder wants attributes names structured like:

  `{ "@name": "Office Theme"} `

rather than

  `{ "@" : { "name" : { value: "Office Theme" }}}`

Should I configure xml2js differently, xmlbuilder differently, or is there a different pair of libraries that can parse XML -> JSON -> XML?

prototype
  • 7,249
  • 15
  • 60
  • 94
  • 1
    Is there a specific reason for this detour to JSON and back? Modifying the XML directly seems the more sensible approach when XML is what you want to have in the end... – Tomalak Feb 24 '15 at 13:51
  • Great question. In this case, the UI and DB work with JS/JSON objects, so the ability to represent XML content (e.g. an MS Office doc) as JSON allows user configurations to be applied by mixing in objects without explicitly traversing DOM. I was hoping the back end would be as simple as `xml2js.parseString(xml, function(err, json) { _.mixin(json, edits); xmlbuilder.create(json).end(); ` – prototype Feb 24 '15 at 16:10

3 Answers3

23

The xml2js package comes with its own XML builder, to which the documentation has to say:

Since 0.4.0, objects can be also be used to build XML:

var fs = require('fs'),
    xml2js = require('xml2js');

var obj = {name: "Super", Surname: "Man", age: 23};
var builder = new xml2js.Builder();
var xml = builder.buildObject(obj);

At the moment, a one to one bi-directional conversion is guaranteed only for default configuration, except for attrkey, charkey and explicitArray options you can redefine to your taste.

So, after dropping your custom parser configuration, this works perfectly for me:

var fs = require('fs');
var path = require('path');
var xml2js = require('xml2js');

xmlFileToJs('theme.xml', function (err, obj) {
    if (err) throw (err);
    jsToXmlFile('theme2.xml', obj, function (err) {
        if (err) console.log(err);
    })
});
// -----------------------------------------------------------------------

function xmlFileToJs(filename, cb) {
    var filepath = path.normalize(path.join(__dirname, filename));
    fs.readFile(filepath, 'utf8', function (err, xmlStr) {
        if (err) throw (err);
        xml2js.parseString(xmlStr, {}, cb);
    });    
}

function jsToXmlFile(filename, obj, cb) {
    var filepath = path.normalize(path.join(__dirname, filename));
    var builder = new xml2js.Builder();
    var xml = builder.buildObject(obj);
    fs.writeFile(filepath, xml, cb);
}
Tomalak
  • 332,285
  • 67
  • 532
  • 628
  • Thanks! Task blindness, somehow with the name `xml2js` I missed it's also `js2xml`. – prototype Feb 24 '15 at 17:37
  • 1
    I noticed that XML comments are dropped when converting from XML to JSON, but I suppose your input XML does not contain mission-critical comments. Be sure to write your file as UTF-8 (it's the default with `fs.writeFile()`, but make it explicit in your code anyway, if only to have it symmetric with `fs.readFile()`). See modified code. – Tomalak Feb 24 '15 at 18:49
  • What is here "cb" and "obj" as an argument in the function? – User123 Dec 28 '20 at 14:18
  • 1
    @User123 `obj` is the JS object (in `xml2js` format) you want to write to the XML file, and `cb` is the callback function you want to have called when the conversion process is done. – Tomalak Dec 28 '20 at 17:51
2

While Converting XML to JSON , The xml2js maps the attributes to '$'. In case if your attributes key Name does not match with the Child Key Name . You could Merge the Attributes with Child elements . HEnce your JSON looks clean.

**

xml2js.Parser({ignoreAttrs : false, mergeAttrs : true})

**

Might Solve your problem.

Sumanth
  • 477
  • 1
  • 6
  • 11
1

I came across similar requirement recently. Had to convert Yahoo weather service xml data into json object. Solved the issue with xml2js and js2xmlparser modules. Thought to share how I did here. An example which I worked out:

const https = require('https');
var parseString = require('xml2js').parseString;

https.get('https://query.yahooapis.com/v1/public/yql?q=select%20*%20from%20weather.forecast%20where%20woeid%20in%20(select%20woeid%20from%20geo.places(1)%20where%20text%3D%22surat%22)&format=xml&env=store%3A%2F%2F    datatables.org%2Falltableswithkeys', (resp) => {
    let data = '';

    // A chunk of data has been recieved.
    resp.on('data', (chunk) => {
        data += chunk;
    });

    // The whole response has been received. Print out the result.
    resp.on('end', () => {
        //console.log(JSON.parse(data).explanation);

        parseString(data, function (err, result) {
            console.log(JSON.stringify(result));
        });
    });

}).on("error", (err) => {
    console.log("Error: " + err.message);
});

After this one can use js2xmlparser for converting the obtained json to xml.

js2xmlparser.parse("weather", data); 
....

here's a Github link for the detailed code: https://github.com/joshiparthin/ReNode/tree/master/soap-api

Parth Joshi
  • 452
  • 4
  • 6