render - formats should include model
Feature description
Currently Grails controllers render json with no model. This is extremely limiting and contrary to how other views work. For instance, def index() provides "${entityName.uncapitalize()Count}" to index.gsp, but index.json just returns a json list with no count.
Meta data is crucial for being able to properly paginate through large amount of results or even providing context. Proper pagination should use cursors instead of offsets and using the default rendering strategy provides no mechanism for providing this meta data.
The default should be changed to a structure that provides data and meta data. A possibility is:
{
"data": [ /* the list */ ],
"meta": { /* the model */ },
}
For example:
{
"data": [ { "name": "Bob", "id": 1 }, ... ],
"meta": { "count": 100, "cursor": "aSdFa123" }
}
https://jsonapi.org/format/#fetching-pagination
def index() provides "${entityName.uncapitalize()Count}" to index.gsp
With which version of Grails does index() provide that?
def index() provides "${entityName.uncapitalize()Count}" to index.gsp
With which version of Grails does index() provide that?
@jeffscottbrown All of them. That logic has been part of the scaffolding plugin for a long time. I have never been a big fan of every controller representing the index count under a different property name.
https://github.com/apache/grails-core/blob/ca47f524c65d20104b80c13d684d1b5c9d905f29/grails-scaffolding/src/main/templates/scaffolding/Controller.groovy#L12-L15
All of them. That logic has been part of the scaffolding plugin for a long time.
Got it. I am not used to seeing that call to uncapitalize() in scaffolded controllers. For example, in 6.2.3 I am seeing this:
def index(Integer max) {
params.max = Math.min(max ?: 10, 100)
respond widgetService.list(params), model:[widgetCount: widgetService.count()]
}
Thank you for the info. That was really helpful.
I didn't know about cursor strategy. How it works?
And related to metadata, I have seen, I have used, and I have implemented rest apis that in response headers there is an attribute named X-Total-Count that, with conjunction with limit and offset querystring parameters is easy to play with pagination.
@muser83 some databases offer cursors so you can efficiently paginate through data without having to scan to the offset. Using offsets is very inefficient for quantities of results.
You could could also simulate cursor behavior by creating an artificial cursor using a multi field sort+id. for instance, you could sort by dateCreated desc, id asc
Spring Data Rest format:
{
"_embedded" : {
"people" : [ {
"_links" : {
"self" : {
"href" : "http://localhost:8080/people/68bb8e74405d67e524d995de"
},
"person" : {
"href" : "http://localhost:8080/people/68bb8e74405d67e524d995de"
}
},
"displayName" : "Ethan",
"ownerUserId" : "68bb8d7dbc9d8e654d81bfe8",
"guardianUserIds" : [ ],
"created" : "2025-09-05T18:29:24.614Z",
"modified" : "2025-10-03T01:23:41.674Z"
} ]
},
"_links" : {
"first" : {
"href" : "http://localhost:8080/people?page=0&size=1"
},
"self" : {
"href" : "http://localhost:8080/people?page=0&size=1"
},
"next" : {
"href" : "http://localhost:8080/people?page=1&size=1"
},
"last" : {
"href" : "http://localhost:8080/people?page=2&size=1"
},
"profile" : {
"href" : "http://localhost:8080/profile/people"
}
},
"page" : {
"number" : 0,
"size" : 1,
"totalElements" : 3,
"totalPages" : 3
}
}
RestfulController also needs to be updated:
https://github.com/apache/grails-core/blob/c9597b6aa7acf74b74e39967992f11dbed4aaa30/grails-rest-transforms/src/main/groovy/grails/rest/RestfulController.groovy#L68-L72