A Grails JSON Builder that doesn’t suck

Posted by: on Jun 19, 2008 | 6 Comments

Now we’ve been doing some fun Ajax bits with JSON recently in Grails. However it has been very painful using the JSON builder. Initially I didn’t want to use the JSON converter (render x as JSON) as the data is coming from several sources, and there are some domain class properties we don’t want to expose.

So the builder approach seemed to make sense. The only trouble is that render(contentType:’text/json’) { … } uses Grails default JSONBuilder which I have discovered sucks, and also blows.

The default JSONBuilder uses method calls for everything. However in some cases the method name is ignored – eg elements of an array. Also you can only set name/value pairs with simple method(value) constructs on the root node of the builder, as in subnodes each one of these creates a new object eg { method: value }. The final blow for me was that property values of anything other than simple types are just rendered as strings. For example a property value that is an array of ints is rendered as a string so "[1, 2, 3, 4]" because JSONWriter has just a def value(Object) method that calls toString(). This is in contrast to the "as JSON" converter construct which works nicely with nested objects and is sane when it comes to recursion/parent node references.

So as soon as you step away from trivial lists and have any kind of object graph to represent, it falls down.

Enough was enough so I knocked this together. It needs a lot more sanity checking in there and possibly some optimisation but it took half an hour and it works. Using it is a case of calling "render new BetterJSONBuilder() { … }".

What it does is use any method calls with a Map to create a new JSON object with those properties. Any method call with a closure creates a new array OR object – if the first call in a closure is to "element(value)" it will create an array. Property access is permitted on object nodes eg you set properties with x=y not with x(y) as in the standard horrible JSONBuilder.

Internally it just creates a tree of maps/lists and then uses "as JSON" to do the conversion. Short and sweet.

class BetterJSONBuilder {

static NODE_ELEMENT = "element"

def root

def current

def nestingStack = []

def build(Closure c) {
c.delegate = this
//c.resolveStrategy = Closure.DELEGATE_FIRST
root = [:]
current = root
c.call()
return root as JSON // requires deep
}

def invokeMethod(String methodName) {
current[methodName] = []
}

def invokeMethod(String methodName, Object args) {
if (args.size()) {
if (args[0] instanceof Map) {
// switch root to an array if elements used at top level
if ((current == root) && (methodName == NODE_ELEMENT) && !(root instanceof List)) {
if (root.size()) {
throw new IllegalArgumentException('Cannot have array elements in root node if properties of root have already been set')
} else {
root = []
current = root
}
}
def n = [:]
if (current instanceof List) {
if (methodName != NODE_ELEMENT) {
throw new IllegalArgumentException('Array elements must be defined with the "element" method call eg: element(value)')
}
current << n
} else {
current[methodName] = n
}
n.putAll(args[0])
} else if (args[-1] instanceof Closure) {
def n = []

nestingStack << current

if (current instanceof List) {
current << n
} else {
current[methodName] = n
}
current = n
args[-1].call()
current = nestingStack.pop()
} else if (ars.size() == 1) {
if (methodName != NODE_ELEMENT) {
throw new IllegalArgumentException('Array elements must be defined with the "element" method call eg: element(value)')
}
// switch root to an array if elements used at top level
if (current == root) {
if (root.size()) {
throw new IllegalArgumentException('Cannot have array elements in root node if properties of root have already been set')
} else {
root = []
current = root
}
}
if (current instanceof List) {
current << args[0]
} else {
throw new IllegalArgumentException('Array elements can only be defined under "array" nodes')
}
} else {
throw new IllegalArgumentException("This builder does not support invocation of [$methodName] with arg list ${args.dump()}")
}
} else {
current[methodName] = []
}
}


void setProperty(String propName, Object value) {
current[propName] = value
}

def getProperty(String propName) {
current[propName]
}
}

Update: I found this morning that I missed out the "import grails.converters.JSON" from this. I also discovered that for me at least, if I imported the deep.JSON converter, I was getting lots of "Value out of sequence" exceptions. Using the normal non-deep JSON converter all is fine and, weirdly, nested objects seem fine too – e.g. I have a top level JS object, a value within it that is an array of other objects, and those objects have an array property also. All rendering fine without deep JSON converter.

6 Comments

  1. Andres Almiray
    June 20, 2008

    Funny you mention it. I’m about to start a grails-json-plugin for JSON producing/consuming based on Json-lib, which among other things provides its own builder http://json-lib.sourceforge.net/apidocs/net/sf/json/groovy/JsonGroovyBuilder.html

    Reply
  2. Sean Harney
    July 14, 2009

    According to http://www.grails.org/doc/1.1.x/api/grails/converters/deep/JSON.html the deep converter is deprecated. The Converters framework has been refactored and this made the deep Converters obsolete

    I was having the same problem as you using the deep converter, it always throws the Value out of sequence JSONException regardless if the data has nested objects or not.

    Reply
  3. Kevin
    September 16, 2009

    Thanks for providing this. Its much better than the default Grails JSONBuilder. For others looking at the code, note that there is a typo around line 55 (“} else if (ars.size() == 1) {“) where ‘ars’ should be ‘args’.

    Marc — What’s the license for this class if I want to use it in my own grails project?

    Reply
  4. Donal
    November 18, 2009

    Surely the correct usage is new BetterJSONBuilder.build { }
    When I try your suggested usage new BetterJSONBuilder { }

    I get an error saying that no constructor that takes a closure parameter exists.

    Reply
  5. Shailen
    December 8, 2009

    Hi Marc,

    Thanks for this: spent many frustrated hours trying all sorts of stuff with the default Builder before I came across your post…

    Thanks again

    Reply
    • Marc Palmer
      December 11, 2009

      FYI Grails 1.2 has a fixed JSON builder based on this work!

      Reply

Leave a Reply