Revisiting the problem
I revisited the problem and my previous answer, and decided to post this one instead of editing the former, addressing here a couple of comments/suggestions received to improve my proposed solution.
I moved presentation logic away from the domain class. To achieve that, I've explored three different approaches:
- Using a Grails Service
- Coding in the JSON View side
- Using only templates
Just for the sake of clarity, I added the following mappings in URLMappings.groovy:
"/approach1"(controller: 'foo', action:'approach1')
"/approach2"(controller: 'foo', action:'approach2')
"/approach3"(controller: 'foo', action:'approach3')
Approach 1: Using a Grails Service
Please note that in FooController the service bean fooService is included as a parameter in the respond call to the JSON View.
FooService.groovy
package rendermapexample
import grails.gorm.transactions.Transactional
@Transactional
class FooService {
def toAllCaps(mealsByPerson) {
mealsByPerson.each { person, breakfast ->
def breakfastMap = [:]
breakfast.properties.each { key, value ->
if (value && value.class == String) {
breakfastMap[key] = value.toUpperCase()
} else {
breakfastMap[key] = value
}
}
mealsByPerson[person] = breakfastMap
}
return mealsByPerson
}
}
FooController.groovy
package rendermapexample
class FooController {
static responseFormats = ['json', 'xml']
def fooService
def approach1() {
Map<String, Breakfast> mealsByPerson = [
Tom: new Breakfast(meat: "bacon", eggs: "scrambled", side: "hashbrowns"),
Jack: new Breakfast(meat: "sausage", eggs: "over easy", side: "pancakes")
]
respond cost: 12.34f,
date: new Date(),
mealsByPerson: mealsByPerson,
allCaps: params.boolean("allCaps"),
fooService: fooService
}
}
approach1.gson
import rendermapexample.Breakfast
import rendermapexample.FooService
model {
Float cost
Date date
Map<String, Breakfast> mealsByPerson
Boolean allCaps
FooService fooService
}
json {
date date
cost cost
mealsByPerson g.render(allCaps ? fooService.toAllCaps(mealsByPerson) : mealsByPerson)
}
Of course, instead of passing the fooService bean as a parameter, we could place toAllCaps code into a POJO static utility class, and then import it in approach2.gson.
Approach 2: Coding in the JSON View side
In case more control is needed on the JSON View side, we could move toAllCaps function from FooService.groovy to approach1.gson, and just discard FooService.groovy.
FooController.groovy
package rendermapexample
class FooController {
static responseFormats = ['json', 'xml']
def approach2() {
Map<String, Breakfast> mealsByPerson = [
Tom: new Breakfast(meat: "bacon", eggs: "scrambled", side: "hashbrowns"),
Jack: new Breakfast(meat: "sausage", eggs: "over easy", side: "pancakes")
]
respond cost: 12.34f,
date: new Date(),
mealsByPerson: mealsByPerson,
allCaps: params.boolean("allCaps")
}
}
approach2.gson
import rendermapexample.Breakfast
model {
Float cost
Date date
Map<String, Breakfast> mealsByPerson
Boolean allCaps
}
json {
date date
cost cost
mealsByPerson g.render(allCaps ? toAllCaps(mealsByPerson) : mealsByPerson)
}
def toAllCaps(mealsByPerson) {
mealsByPerson.each { person, breakfast ->
def breakfastMap = [:]
breakfast.properties.each { key, value ->
if (value && value.class == String) {
breakfastMap[key] = value.toUpperCase()
} else {
breakfastMap[key] = value
}
}
mealsByPerson[person] = breakfastMap
}
return mealsByPerson
}
Approach 3: Using only templates
Here I intended to do less traditional groovy coding, and rely more on JSON View templates, as outlined in the official documentation.
Please note the following caveats:
I'm using an ArrayList variant of mealsByPerson because some features of JSON View templates require an object that implements the Iterator interface. Important: This results in a JSON Array of separate, individual objects rather than a single JSON object containing all the map entries as described in the original question.
I had to disable static compilation for JSON Views. This is because some of the JSON object names within mealsByPerson are dynamic (i.e. they are not just labels but actual data). Namely "Tom" and "Jack" object names from the original post.
application.yml
grails:
views:
json:
compileStatic: false
FooController.groovy
package rendermapexample
class FooController {
static responseFormats = ['json', 'xml']
def approach3() {
ArrayList mealsByPerson = [
Tom: new Breakfast(meat: "bacon", eggs: "scrambled", side: "hashbrowns"),
Jack: new Breakfast(meat: "sausage", eggs: "over easy", side: "pancakes")
].collect()
respond cost: 12.34f,
date: new Date(),
mealsByPerson: mealsByPerson,
allCaps: params.boolean("allCaps")
}
}
approach3.gson
import rendermapexample.Breakfast
model {
Float cost
Date date
ArrayList mealsByPerson
Boolean allCaps
}
json {
date date
cost cost
mealsByPerson tmpl.mealsByPerson(mealsByPerson, [allCaps: allCaps])
}
_mealsByPerson.gson
import rendermapexample.Breakfast
model {
Map.Entry mealsByPerson
Boolean allCaps
}
String person = mealsByPerson.key
Breakfast breakfast = mealsByPerson.value
json {
"${person}" tmpl.breakfast(breakfast:breakfast, allCaps:allCaps)
}
_breakfast.gson
import rendermapexample.Breakfast
model {
Breakfast breakfast
Boolean allCaps
}
json {
if (allCaps) {
meat breakfast.meat.toUpperCase()
eggs breakfast.eggs.toUpperCase()
side breakfast.side.toUpperCase()
} else {
meat breakfast.meat
eggs breakfast.eggs
side breakfast.side
}
}