Passing an array of arrays (or other JSON-like data) to a macro

Is there a straightforward way to pass an array of arrays, or even better, something approximating an arbitrary JSON-like object to a macro?

Consider the following code:

{{velocity}}

#set($myvar = [ [0,0], [1,2] ] )
$myvar.class.name
#foreach($i in $myvar)
$i
#foreach($j in $i)
$j
#end
#end

{{testMacro param=“[0,0], [1,2]”/}}

{{/velocity}}

The code in testMacro is essentially the same:

{{velocity}}
INSIDE TESTMACRO
#set($myvar = $wikimacro.parameters.param)
$myvar.class.name
#foreach($i in $myvar)
$i
#foreach($j in $i)
$j
#end
#end

{{/velocity}}

The parameter “param” is defined to be of type “java.util.ArrayList”.

Here’s the output:

java.util.ArrayList
[0, 0]
0
0
[1, 2]
1
2

INSIDE TESTMACRO
java.util.ArrayList
[0
0]
[1
2]

So: in the first set of code, the #set() is correctly parsing a list of lists. However, when passing the same thing to the macro, the parser is not paying any attention to the structure inside the list, just doing a dumb comma separation.

I have likewise been having difficulties passing a map to a macro.

Is it possible to pass data objects like this to a macro, and do you have any examples that show how to do it?

Your testMacro is rendered after the Velocity code is executed. The text output resulted from the evaluation of the Velocity code is parsed and rendered as wiki syntax, so the values passed to the rendering macro parameters are strings by default. Now, the rendering framework will try to convert the value from String to the specified macro parameter type if there is a converter. In your case it will covert the string “[0,0], [1,2]” to an ArrayList by taking the comma separated values, as you noticed.

You can try to set the macro parameter type to java.util.ArrayList<java.util.ArrayList> but I doubt there is a converter available for this type. So in the end you probably need to serialize the parameter value (whatever its real type is) to some string format before calling the macro and then parse that string inside the macro code. Basically to consider the macro parameter as string. Otherwie you have to implement the missing converter (in Java) between String and the macro parameter type you want to use.

This is definitely going to cause me some challenges. We are migrating from a Mindtouch installation and this sort of thing is trivial there, and so I used it a lot. It makes it very easy to use macros to generate formatted output given a set of easy-to-enter specifications in a JSON-like structure. Under no circumstance should the page that calls the macro be responsible for writing additional code to package up the data; that’s basically another way of saying “don’t use this macro”.

And so that means that, like you say, I’ll need to pass it in as a string and parse it in the macro, if that’s even possible. Is there something that will parse a JSON string into an object? I haven’t yet figured out how to search the Java docs productively to find stuff like this.

Failing that I may have to resort to passing the string to some Javascript code and do it all client side after the page is loaded. That wouldn’t be a disaster but certainly not my first choice.

If the data is static then you (the user) can do the string serialization in your head and write down the result, i.e. you can write down the JSON string as the macro parameter value (making sure the quotes don’t conflict with the macro parameter syntax).

{{myMacro config="{'limit': 10, 'offset': 0}" /}}

If the data is dynamic then you need to use an API to perform the string serialization.

#set ($config = $services.foo.readConfig(...))
{{myMacro config='$jsontool.serialize($config)' /}}

Is there something that will parse a JSON string into an object?

Checkout the $jsontool on https://extensions.xwiki.org/xwiki/bin/view/Extension/Velocity+Module#HVelocityTools .

Thank you.

Temporarily, I am using a string parameter and passing everything to JavaScript, and that works. But jsontool is what I really want (dunno why I didn’t see it before).

I haven’t gotten it to work yet, though; it’s acting like jsontool simply isn’t there. I checked in the admin panel and confirmed the Velocity API was installed. Here’s my test Macro code:

{{velocity}}
INSIDE TESTMACRO
$wikimacro.parameters.param
$JSONTool.fromString($wikimacro.parameters.param)
#set($myvar = $JSONTool.fromString($wikimacro.parameters.param))
$myvar
$myvar.class.name
#foreach($i in $myvar)
$i
$i.class.name
#end
{{/velocity}}

Here’s the output I get:

INSIDE TESTMACRO
{‘hello’: { ‘there’: ‘!’ }, ‘array’: [ 0,1,2 ]}
$jsontool.fromString($wikimacro.parameters.param)
$myvar
$myvar.class.name

I’ve tried this both with jsontool all lower case 9as shown in your sample code and also JSONTool, as shown in the docs. Same result either way. Is there something obvious I’m doing wront here?

Velocity is case sensitive, the name of the tool is $jsontool (JSONTool is the Java class name) and you need to pass valid JSON (strict JSON expects double quotes so you’ll have to use single quotes to wrap the macro parameter value).

{{velocity}}
## See if $jsontool is available. Velocity is case sensitive!
$jsontool
## See if fromString works with an empty object
$jsontool.fromString('{}')
## Try with a more complex object
$jsontool.fromString('{"a":"b"}')
## Try with invalid JSON and check the server logs
$jsontool.fromString("{'a':'b'}")
## Failed to parse JSON [{'a':'b'}]: JsonParseException: Unexpected character (''' (code 39)): was expecting double-quote to start field name at [Source: (String)"{'a':'b'}"; line: 1, column: 3]
{{/velocity}}

Thanks, that did it for me. I was not expecting the requirement for strict JSON, that’s a bit frustrating but I’ll have to live with it. Also the fact that if the fromString method encounters an error then it looks no different from if $jsontools doesn’t exist at all.

So it appears I can do some halfway decent validation like this:

#set ($myvar = ${ jsontools.fromString($wikimacro.parameters.param) | “ERROR” } )
#if ($myvar == “ERROR”)
Invalid JSON: $wikimacro.parameters.param
#end

or somesuch. Janky, but it should work. Is there a more robust and/or “correct” way to catch an error in a method call in Velocity?

if you need a try/catch, see https://extensions.xwiki.org/xwiki/bin/view/Extension/Velocity%20Module#HVelocityDirectives

Thanks for the pointer, I’ll give that a go.