Tutorial: the TreatAsArray option in the XMLToJSON policy

In Apigee Edge, XMLToJSON is one of the key foundational mediation policies that enables modernization of existing SOAP-based or XML-based services. The ability to convert payloads between JSON to XML on the request, and between XML to JSON on the response, means that "old" angle-bracket services can get a facelift and become easier to use by webapps and just about everybody else.

As I have pointed out elsewhere, the XMLToJSON policy uses a heuristic to decide whether the element being converted should turn into a simple object in the JSON, or an array of objects: if there are multiple child elements with the same name, then obviously it is an array. If there is one child element, then not an array. This naive heuristic works, sort of, but it's got a glaring weakness: when arrays sometimes have just a single element.

Imagine multiple different requests, and sometimes the payload has a single element, and *sometimes* has two or more of those elements. When that happens, XMLToJSON converts the payloads differently, and it results in a different programming model on the client side. The client code has to handle that case.

Let's have a closer look at what I mean. Suppose you have a payload that sometimes looks like this:

<Base>
  <Item>
    <name>pod1</name>
    <region>us-east-1</region>
  </Item>
  <Item>
    <name>pod2</name>
    <region>us-west-2</region>
  </Item>
</Base>

..and sometimes looks like this:

<Base>
  <Item>
    <name>pod1</name>
    <region>us-east-1</region>
  </Item>
</Base>

The XMLToJSON will convert the former example to this:

{
  "Base": {
    "Item": [{
      "name": "pod1",
      "region": "us-east-1"
    }, {
      "name": "pod2",
      "region": "us-west-2"
    }]
  }
}

You can see that the Item property is an array. The latter will convert to something like this:

{
  "Base": {
    "Item": {
      "name": "pod1",
      "region": "us-east-1"
    }
  }
}

And here, Item is not an array.

Suppose you have a client-side app that needs to parse the resulting JSON. It runs in a browser and the code is JavaScript. The JS to handle this JSON will need to be defensively coded:

var myjson = JSON.parse(payload);
var type = Object.prototype.toString.call( myjson.Base.Item );
if( type === '[object Array]' ) {
    // handle the Item as an array
}
else if ( type === '[object Object]' ) { 
    // handle the Item as an single JS hash
}

Gah! That's not a very nice thing to force upon a client app developer.

To address that problem, we've recently extended the XMLToJSON policy so that the policy configuration can specify that one or more elements in the XML should always be converted to an array in the output JSON.

Here's what the configuration looks like:

<XMLToJSON name="XML-to-JSON">
    <Options>
        <RecognizeNull>true</RecognizeNull>
        <TextNodeName>#text</TextNodeName>
        <AttributePrefix>@</AttributePrefix>
        <TreatAsArray> <!-- new element -->
            <Path unwrap="true">Base/Item</Path>
        </TreatAsArray>
    </Options>
    <OutputVariable>response</OutputVariable>
    <Source>response</Source>
</XMLToJSON>

With that policy, the latter XML example above will be converted to this:

{
  "Base": {
    "Item": [{
      "name": "pod1",
      "region": "us-east-1"
    }]
  }
}

This will make the lives of your client app developers, just ever-so-slightly better.

To get a screencast view of this feature, see here.

6100-screenshot-20171208-104202.png

Comments
Not applicable

@Dino - when will this be available in Private Cloud? Is there a workaround otherwise? I am having this very issue and this new feature would be very helpful. Thanks!

DChiesa
Staff

Hi Don,

This is available in ... 16.01.06, 16.05.05, or 16.09.00 .

vishalvekaria19
Explorer

 

@DChiesa 

I have this same use case but its not giving output as mentioned above


Case 1:-
<Search>
     <Query>
          <FromTime>XXX</FromTime>
          <TotTme>XXX</TotTme>
          <Room>
               <ID>XXX</ID>
               <AVAL>XXX</AVAL>
               <Type>XXX</Type>
         </Room>
          <Room>
               <ID>XXX</ID>
               <AVAL>XXX</AVAL>
               <Type>XXX</Type>
          </Room>
 </Query>
</Search>
Output:-
{
"search": {
  "query": {
    "FromTime": "XXX",
    "TotTme": "XXX",
    "Room": [
      {
        "Id": "XXX",
        "AVAL": "XXX",
        "Type": "XXX"
       },
       {
         "Id": "XXX",
         "AVAL": "XXX",
         "Type": "XXX"
        }
    ]
  }
}
}

I have set the path in <TreatAsArray> under <Options>
<Options>
<TreatAsArray>
<Path>Search/Query/Room</Path>
</TreatAsArray>
<RecognizeNumber>true</RecognizeNumber>
</Options>

But in Case2 I am not getting correct output:-
<Search>
     <Query>
          <FromTime>XXX</FromTime>
          <TotTme>XXX</TotTme>
          <Room>
               <ID>XXX</ID>
               <AVAL>XXX</AVAL>
               <Type>XXX</Type>
         </Room>
 </Query>
</Search>

output:-
{
"Search": {
"Query": {
"FromTime": "XXX",
"TotTme": "XXX",
"Room": {
"ID": "XXX",
"AVAL": "XXX",
"Type": "XXX"
}
}
}
}

instead it should be
{
"Search": {
"Query": {
"FromTime": "XXX",
"TotTme": "XXX",
"Room": [
{
"ID": "XXX",
"AVAL": "XXX",
"Type": "XXX"
}
]
}
}
}

Version history
Last update:
‎10-15-2016 02:34 PM
Updated by: