The original answer didn't work for me. There may have been a change in the apps script XML API but it wouldn't include the text content of a node without children. Here is the code I wrote that seems to work well.
Note, it outputs in a slightly different fashion than the example you provided. I found that this might be a more consistent format for a broader range of use cases. I also found that including the attributes wasn't necessary for everything I was doing and created clutter, so I've included a version that doesn't parse attributes.
If you include attributes, the output follows this pattern:
{foo:{attributes:{...},content:{...}}
To Include Attributes:
function xmlParse(element) {
/*
* Takes an XML element and returns an object containing its children or text
* If children are present, recursively calls xmlTest() on them
*
* If multiple children share a name, they are added as objects in an array
* If children have unique names, they are simply added as keys
* i.e.
* <foo><bar>one</bar><baz>two</baz></foo> === {foo: {bar: 'one', baz: 'two'}}
* <foo><bar>one</bar><bar>two</bar></foo> === {foo: [{bar: 'one'},{bar: 'two'}]}
*/
let obj = {}
const rootName = element.getName();
// Parse attributes
const attributes = element.getAttributes();
const attributesObj = {};
for(const attribute of attributes) {
attributesObj[attribute.getName()] = attribute.getValue();
}
obj[rootName] = {
attributes: attributesObj,
content: {}
}
const children = element.getChildren();
const childNames = children.map(child => child.getName());
if (children.length === 0) {
// Base case - get text content if no children
obj = {
content: element.getText(),
attributes: attributesObj
}
} else if (new Set(childNames).size !== childNames.length) {
// If nonunique child names, add children as an array
obj[rootName].content = [];
for (const child of children) {
if (child.getChildren().length === 0) {
const childObj = {};
childObj[child.getName()] = xmlParse(child);
obj[rootName].content.push(childObj)
} else {
const childObj = xmlParse(child);
obj[rootName].content.push(childObj)
}
}
} else {
// If unique child names, add children as keys
obj[rootName].content = {};
for (const child of children) {
if (child.getChildren().length === 0) {
obj[rootName].content[child.getName()] = xmlParse(child);
} else {
obj[rootName].content = xmlParse(child);
}
}
}
return obj;
}
Without Attributes:
function xmlParse(element) {
/*
* Takes an XML element and returns an object containing its children or text
* If children are present, recursively calls xmlTest() on them
*
* If multiple children share a name, they are added as objects in an array
* If children have unique names, they are simply added as keys
* i.e.
* <foo><bar>one</bar><baz>two</baz></foo> === {foo: {bar: 'one', baz: 'two'}}
* <foo><bar>one</bar><bar>two</bar></foo> === {foo: [{bar: 'one'},{bar: 'two'}]}
*/
let obj = {}
const rootName = element.getName();
const children = element.getChildren();
const childNames = children.map(child => child.getName());
if (children.length === 0) {
// Base case - get text content if no children
obj = element.getText();
} else if (new Set(childNames).size !== childNames.length) {
// If nonunique child names, add children as an array
obj[rootName] = [];
for (const child of children) {
if (child.getChildren().length === 0) {
const childObj = {};
childObj[child.getName()] = xmlParse(child);
obj[rootName].push(childObj)
} else {
const childObj = xmlParse(child);
obj[rootName].push(childObj)
}
}
} else {
// If unique child names, add children as keys
obj[rootName] = {};
for (const child of children) {
if (child.getChildren().length === 0) {
obj[rootName][child.getName()] = xmlParse(child);
} else {
obj[rootName] = xmlParse(child);
}
}
}
return obj;
}
Usage for both of these:
const xml = XmlService.parse(xmlText);
const rootElement = xml.getRootElement();
const obj = xmlParse(rootElement);
const asJson = JSON.stringify(obj);
Reference:
XMLService