Sorting numeric-string-keyed objects in Velocity (like they would sort in JS)

One of our clients recently did a big import of JSON-formatted values into a Textarea field. The result in Velocity, after the usual #evaluate trick to rehydrate JSON, is equivalent to this:

#set( $stringKeyedObject = {
  "0": "apples",
  "100": "oranges",
  "2": "pears"
} )

(The data is structured like that for an important reason I’ll explore in a near-future post.)

Here’s the thing: the system that exported this data uses NodeJS. In JS, creating an object literal like

let o = {
  "0": "apples",
  "100": "oranges",
  "2": "pears"
};

and looping over the keys with

Object.entries(o).forEach( ([key,value]) => console.log(key,value) )

uses a specific, standards-defined order. Because the keys are all numeric strings (the term is array indexes in the ES standard), the entries are sorted as numbers, not in insertion order:

0 apples
2 pears
100 oranges

This differs starkly from Velocity. In Velocity, for an identical-looking object literal, the order in a #foreach loop is insertion order:

0 apples
100 oranges
2 pears

That’s because Velocity object literals always create a LinkedHashMap, an object that maintains insertion order, regardless of the type of key.

The problem, then, is the source system thinks the JS order will be used to output values in a Marketo email, regardless of the order of the keys in the JSON. But that’s wrong!

Sorting it out

The solution is kind of clunky, but works fine. First, transform the object into an array of objects where each object has the keys "idx" and "itm":

#set( $numericKeyedEntries = [] )
#foreach( $entry in $stringKeyedObject.entrySet() )
#set( $numericKeyedEntry = {
  "idx" : $convert.toInteger($entry.getKey()), 
  "itm" : $entry.getValue()
})
#set( $void = $numericKeyedEntries.add( $numericKeyedEntry ) )
#end

"idx" is the original numeric string key, cast to an Integer; the "itm" is the original value. So $numericKeyedEntries looks like this:

[
{
  "idx" : 0,
  "itm" : "apples"
}, 
{
  "idx" : 100,
  "itm" : "oranges"
},  
{
  "idx" : 2,
  "itm" : "pears"
}
];

Now, you can use SortTool to sort on the "idx" property, which will be sorted numeric-ascending since it’s an Integer:

#set( $sortedNumericKeyedEntries = $sorter.sort($numericKeyedEntries,"idx") )

And finally, iterate over the objects in the original intended order:

#foreach( $entry in $sortedNumericKeyedEntries ) 
## $entry.idx is the index
## $entry.itm is the value
#end