grails-core icon indicating copy to clipboard operation
grails-core copied to clipboard

render - formats should include model

Open codeconsole opened this issue 7 months ago • 7 comments

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

codeconsole avatar May 22 '25 23:05 codeconsole

def index() provides "${entityName.uncapitalize()Count}" to index.gsp

With which version of Grails does index() provide that?

jeffscottbrown avatar Jun 02 '25 13:06 jeffscottbrown

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

codeconsole avatar Jun 03 '25 02:06 codeconsole

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.

jeffscottbrown avatar Jun 04 '25 12:06 jeffscottbrown

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 avatar Jun 04 '25 12:06 muser83

@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

codeconsole avatar Jun 10 '25 04:06 codeconsole

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
  }
}

codeconsole avatar Oct 06 '25 01:10 codeconsole

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

codeconsole avatar Oct 06 '25 01:10 codeconsole