Search code examples
jsongrailsmicronaut

How to Render a Map as a property in a Grails 4 JSON View


I'm trying to render a JSON view that has a property that is a Map. I would like it to render in JSON as a sub-object. This seems like it should be very easy, but I can't find it documented anywhere. I've created an example project on GitHub.

Domain Class

class Breakfast {
    String meat
    String eggs
    String side
}

Controller

class FooController {
    static responseFormats = ['json', 'xml']
    
    def index() {
        Map<String, Breakfast> mealsByPerson = [
            Tom: new Breakfast(meat: "bacon", eggs: "scrambled", side: "hashbrowns"),
            Jack: new Breakfast(meat: "sausage", eggs: "over easy", side: "pancakes")
        ]

        render template: "foo", model: [cost: 12.34f, date: new Date(), mealsByPerson: mealsByPerson]
    }
}

/foo/_foo.gson

import rendermapexample.Breakfast

model {
    Float cost
    Date date
    Map<String, Breakfast> mealsByPerson
}

json {
    date date
    cost cost
    mealsByPerson mealsByPerson // HOW DO I RENDER THIS
}

This is the JSON I want to render

{
    "cost": 12.34,
    "date": "2021-09-25T01:11:39Z",
    "mealsByPerson": {
        "Tom": {
            "eggs": "scrambled",
            "meat": "bacon",
            "side": "hashbrowns"
        },
        "Jack": {
            "eggs": "over easy",
            "meat": "sausage",
            "side": "pancakes"
        }
    }
}

This is the Error I get

java.lang.reflect.InvocationTargetException: null
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at org.grails.core.DefaultGrailsControllerClass$ReflectionInvoker.invoke(DefaultGrailsControllerClass.java:211)
    at org.grails.core.DefaultGrailsControllerClass.invoke(DefaultGrailsControllerClass.java:188)
    at org.grails.web.mapping.mvc.UrlMappingsInfoHandlerAdapter.handle(UrlMappingsInfoHandlerAdapter.groovy:90)
    at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1040)
    at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:943)
    at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006)
    at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:898)
    at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883)
    at org.grails.web.servlet.mvc.GrailsWebRequestFilter.doFilterInternal(GrailsWebRequestFilter.java:77)
    at org.grails.web.filters.HiddenHttpMethodFilter.doFilterInternal(HiddenHttpMethodFilter.java:67)
    at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)
    at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)
    at java.base/java.lang.Thread.run(Thread.java:829)
Caused by: org.grails.web.servlet.mvc.exceptions.ControllerExecutionException: Error rendering view: null
    at grails.artefact.controller.support.ResponseRenderer$Trait$Helper.renderViewForTemplate(ResponseRenderer.groovy:617)
    at grails.artefact.controller.support.ResponseRenderer$Trait$Helper.render(ResponseRenderer.groovy:353)
    at rendermapexample.FooController.index(FooController.groovy:12)
    ... 15 common frames omitted
Caused by: grails.views.ViewRenderException: Error rendering view: null
    at grails.views.AbstractWritableScript.writeTo(AbstractWritableScript.groovy:43)
    at grails.views.mvc.GenericGroovyTemplateView.renderMergedOutputModel(GenericGroovyTemplateView.groovy:73)
    at org.springframework.web.servlet.view.AbstractView.render(AbstractView.java:317)
    at grails.artefact.controller.support.ResponseRenderer$Trait$Helper.renderViewForTemplate(ResponseRenderer.groovy:614)
    ... 17 common frames omitted
Caused by: java.lang.StackOverflowError: null
    at grails.plugin.json.converters.InstantJsonConverter.handles(InstantJsonConverter.groovy:18)
    at grails.plugin.json.builder.DefaultJsonGenerator.findConverter(DefaultJsonGenerator.java:457)
    at grails.plugin.json.builder.DefaultJsonGenerator.writeObject(DefaultJsonGenerator.java:195)
    at grails.plugin.json.builder.DefaultJsonGenerator.writeMapEntry(DefaultJsonGenerator.java:419)
...

I can't do this:

The render statement below creates the JSON I want in this simple example, but my real-world problem needs more control over rendering than as JSON provides

render ([cost: 12.34f, date: new Date(), mealsByPerson: mealsByPerson] as JSON)

Example Project

https://github.com/tonyerskine/rendermapexample

Update:

I posted a related question here in response to a comment below: How do I render a map of domain objects using a Grails 4 JSON View


Solution

  • If you change _foo.gson to the following:

    import rendermapexample.Breakfast
    
    model {
        Float cost
        Date date
        Map<String, Breakfast> mealsByPerson
    }
    
    json {
        date date
        cost cost
        mealsByPerson g.render(mealsByPerson) {}
    }
    

    The response will include the following:

    {
    "cost": 12.34,
    "date": "2021-09-27T17:24:14Z",
    "mealsByPerson": {
        "Jack": {
            "eggs": "over easy",
            "meat": "sausage",
            "side": "pancakes"
        },
        "Tom": {
            "eggs": "scrambled",
            "meat": "bacon",
            "side": "hashbrowns"
        }
    }