4
votes

I would like to render a complex type, using the JSON render method in Grails, similar to the JSON output below:

{
  "authors": [{
    "id": 1,
    "name": "Author 1",
    "books": [{
      "id": 1,
      "name": "Book 1"
    }, {
      "id": 2,
      "name": "Book 2"
    }]
  }, {
    "id": 2,
    "name": "Author 2",
    "books": [{
      "id": 1,
      "name": "Book 1"
    }, {
      "id": 2,
      "name": "Book 2"
    }]
  }]
}

And I've tried doing this with the following code, where Author and Book are domain classes containing the properties id and name and where Author hasMany Books (association).

def results = Authors.list()
render(contentType:"text/json") {
  authors = array {
    for(a in results) {
      author id:a.id, name:a.name, books: array = {
        def bookresults = Book.findAllByAuthor(a)
        for(b in bookresults) {
          book id:b.id, name:b.name
        }
      }
    }
  }    
}

It works fine with authors alone, but not when I try to iterate through each author's books and render those as well, the code fail.

Any ideas?

Updated question with final code

Thanks to Dave's answer I ended up with the following code that works as expected:

def authors = []

for (a in Author.list()) {
  def books = []
  def author = [id:a.id, name:a.name, books:books]

  for (b in Book.findAllByAuthor(a)) {
    def book = [id:b.id, name:b.name]
    books << book
  }

  authors << author
}

def items = [authors:[authors]]
render items as JSON 
1

1 Answers

10
votes

I find the JSON builder very difficult to use to get the desired results so I prefer to generate the results in maps and lists and then render those.

By having something like the following (warning: untested code!):

def results = []
Authors.list()?.each{ author ->
    def authorResult = [id:author.id, name:author.name]
    Book.findAllByAuthor(author)?.each { book ->
        authorResultput('books', [id:book.id, name:book.name])
    }
    results << authorResult
}
def authors = [authors: results]
render authors as JSON

I think you make the code easier to read and reuse and it should do what you want (my typos permitting).

If you're always going to render your authors and books in the same JSON format you might consider registering a custom JSON object marshaller in Bootstrap.groovy. In summary something like would work:

    JSON.registerObjectMarshaller(Author) {
        def returnArray = [:]
        returnArray['id'] = it.id
        returnArray['name'] = it.name
        return returnArray
    }

Also having a books property on your author would make things easier still!