Skip to content

XMLNode.toObject

do- edited this page May 13, 2022 · 7 revisions

XMLNode.toObject is a tool for transforming XMLNodes to plain, JSON.stringify ready javaScript Objects.

It's somewhat similar to XMLNode's detach method, but, in contrast, merges attributes with child nodes and may alter the original document node order, which is convenient for many data processing scenarios, but is unacceptable for general document transformation tasks.

Technically, this is a function generator: a function that, given a bag of options, returns a function (to be used as XMLReader's map option).

Though exposed as XMLNode's global method for code readability reasons, it's a separate module that may be required directly by his historical name MoxyLikeJsonEncoder reflecting the fact that the basic functionality is modelled after MOXy's one.

const {XMLReader, XMLNode} = require ('xml-toolkit')

const sax = new XMLReader ({
  stripSpace: true,
  collect: e => true,
  filterElements: e => e.localName === 'MessagePrimaryContent',
  map: XMLNode.toObject ({
//      wrap: true,
//   getName: (localName, namespaceURI) => localName.toLowerCase ()
//       map: e => ({id: e.Id, label: e.Name}),
    })
})

Options

Name Default Description
wrap false If true, top object with element name as key is generated
getName (localName, namespaceURI) => localName XML name to property name transforming function
map If set, is applied to the resulting object (think Array.map)

Synopsis

In short, any xml element

<parent attr1="val1" attr2="val2">
  <singleChild>200</singleChild>
  <multiChild k="v"/>
  <multiChild>OK</multiChild>
  <multiChild />
</parent>

is transformed to a js Object

{parent: // omitted with wrap: false option
  {
    attr1: "val1", 
    attr2: "val2", 
    singleChild: "200",
    multiChild: [{k: "v", "OK", null}] 
  }
}

Rules by node type

Top Element

Elements not having any parents are represented by js objects with single key-value pair, where the key is the element name and the value is the js object representing its content:

<MyElement <!--...content...--> </MyElement>    ->    {MyElement: /* ...content...  */}

But, unless the wrap option is explicitly set to true, the top object is not present in the output, so only the content object goes downstream:

<MyElement <!--...content...--> </MyElement>    ->    /* ...content...  */

This is the default behavior because, in most cases, the top element name is a known costant, so copying it have no practical sense.

Attributes

Element attributes are translated into the key-value pairs in his content object:

<MyElement id="1" />    ->    {MyElement: {id: "1"}} // wrap: true
<MyElement id="1" />    ->                {id: "1"}  // default

Note that the value is string, not a number. More on this in the Scalars section.

Text

Child text node of an element is translated to a string representing its content:

<MyElement>1970-01-01</MyElement>    ->    {MyElement: "1970-01-01"} // wrap: true
<MyElement>1970-01-01</MyElement>    ->                "1970-01-01"  // default

XML elements with both text content and attibutes/child elements cannot be processed properly.

Nested elements

Each child element is treated the same way as the top one, except for it is always added as key-value pair in the content object representing its parent:

<MyElement><id>1</id></MyElement>    ->    {MyElement: {id: "1"}} // wrap: true
<MyElement><id>1</id></MyElement>    ->                {id: "1"}  // default

So, for instance, child elements with text content give out same results as attributes.

Multiple values

Sequence of nodes with the same name and same parent are translated into one key-value pair, where the key is their name and the value is the array of their content representations:

<MyElement> <id>1</id> <id>2</id> </MyElement>    ->    {MyElement: {id: ["1", "2"]}} // wrap: true
<MyElement> <id>1</id> <id>2</id> </MyElement>    ->                {id: ["1", "2"]}  // default

Arrays are generated only for multiple homonymous children. There is no way to force the transformer to expose some single valued field as an array.

Notes

Scalars

All leaf scalar values are:

  • either null;
  • either Strings starting and ending with non-blank characters.

No attempt is made to parse text content as Number, Date, Boolean etc.

Each string is trimmed down and, if empty, replaced by null.

Null values

Zero length strings are converted to null values (as in Oracle Database). For attributes, this is done by AttributesMap.set:

<MyElement id="" />    ->    {MyElement: {id: null}} // wrap: true
<MyElement id="" />    ->                {id: null}} // default

For an element without a single child node (attribute, text or nested element), the content is null:

<MyElement></MyElement>, or <MyElement/>  ->    {MyElement: null} // wrap: true
<MyElement></MyElement>, or <MyElement/>  ->                null  // default

Name mapping

By default, local names are used for both elements and attributes, namespaces are ignored (in this section, we assume wrap option to be set on):

<x:MyElement x:ID="1" xmlns:x="uri://..." /> -> 
{
  MyElement: {
    ID: "1"
  }
}

This behaviour can be altered with the getName option. For example:

...
getName: (localName, namespaceURI) => '{' + namespaceURI + '}') + localName.toLowerCase (),
...

<x:MyElement x:ID="1" xmlns:x="uri://..." /> -> 
{
  "{uri://...}myelement": {
    "{uri://...}id": "1"
   }
}

Result mapping

The map option makes it possible to apply some transfortation to each resulting object. Consider an example (this time, wrap is off):

<MyElement id="1" label="Sample" /> -> {id: "1", label: "Sample"}

Having set

map: o => ({...o, is_deleted: false}),

we'll obtain

<MyElement id="1" label="Sample" /> -> {id: "1", label: "Sample", is_deleted: false}
Clone this wiki locally