More Grails Filter Tricks: JSONify Controller Actions

Post by Josh Reed

Building on the previous post, we can use the same filter tricks to clean up some other controller smells. When building REST APIs, it’s a common requirement to output your data in JSON and/or XML. This is usually accomplished using Content Negotiation and the withFormat method:

class BookController {
    def list() {
        def books = Book.list()
        withFormat {
            html bookList: books
            json { render books as JSON }
        }
    }
}

Both of the formats in the withFormat block are working with the same list of books, just rendering them differently. We can use the same after filter trick to clean this up a bit:

jsonify(controller: '*', action: '*') {
    after = { Map model ->
        def accept = request.getHeader('Accept')
        if (!accept.contains('application/json')) { return true }

        // find our controller to see if the action is jsonified
        def artefact = grailsApplication
            .getArtefactByLogicalPropertyName("Controller", controllerName)
        if (!artefact) { return true }

        // check if our action is jsonified
        def isJsonified = artefact.clazz.declaredFields.find {
            it.name == 'jsonify' && isStatic(it.modifiers)
        } != null

        def jsonified = isJsonified ? artefact.clazz?.jsonify : []
        if (actionName in jsonified || '*' in jsonified) {
            // check if we can unwrap the model (such in the case of a show)
            if (model.size() == 1) {
                def nested = model.find { true }.value
                render nested as JSON
            } else {
                render model as JSON
            }
            return false
        }
        return true
    }
}

This filter has a similar structure as the ajaxify one. Instead of looking for an AJAX request, we check to see if the Accept header is ‘application/json‘. Next we check whether the controller has a static jsonify = [...] declaration that includes the action being called. If so, we render the model as JSON. The filter also tries to be a bit smart. If the model only has a single key, e.g. [bookInstance: book], it’ll render just the value. This results in a more natural output:

{"title":"The Stand","author":"Stephen King"}

vs

{"bookInstance":{"title":"The Stand","author":"Stephen King"}}

With the filter, our controller actions can go back to their standard, unadulterated form in favor of a jsonify declaration:

class BookController {
    static jsonify = ['list', 'show']

    def list() {
        [bookList: Book.list()]
    }
    ...
}

Conclusion

This is another way to use filters to keep your controller actions cleaner. It should be noted that the withFormat looks at more than just the Accept header when deciding what format was requested. It also looks at the URL for an explicit format=json URL parameter and for a filename extension, e.g. /book/show/1.js. The filter as written only uses the Accept header, which is common practice when building APIs, but you should evaluate whether that will fit your needs.

This entry was posted in Software Development and tagged , , . Bookmark the permalink.

Related Posts:

9 Responses to More Grails Filter Tricks: JSONify Controller Actions

  1. Rick Jensen says:

    While I like not having to have withFormat blocks littered all over my controllers, it seems like this solution could be improved by not duplicating the logic that the withFormay block encapsulates, namely the determination of when to render something as JSON.

  2. Josh Reed says:

    Hey Rick,

    I don’t quite follow what you’re getting at so please feel free to clarify/correct me.

    I don’t duplicate the logic (as I pointed out in the last paragraph) because I’m only looking at the Accept header instead of the various things that withFormat looks for. For my application, condensing all of this into a single spot instead of multiple withFormats across multiple controllers was a win, even if it meant I was duplicating part of what withFormat is intended to do. That may not be the case for you.

    One obvious improvement, and perhaps you were getting at this, is to dig into the implementation of withFormat to see if there was a re-usable utility method or service call that would do all of the checks that it does to determine whether to render JSON. This would allow the filter to also understand that you want JSON for links like /book/show/1.js or /book/show/1?format=json

    Cheers,
    Josh

  3. Rick Jensen says:

    Yup, it’s the last part that I was getting at. Particularly for our applications, the API is required to be as flexible as possible, so allowing the content type to be set as flexibly as possible is key.

    If you can leverage the logic used by withFormat for checking all ways to indicate desired content type, then you would slim the filter way down since it wouldn’t need to do those checks.

    You could also get some wins by pulling out the logic that makes single-object responses more natural, since that logic would also be useable in other response types, like XML.

  4. Josh Reed says:

    Turns out response.getFormat() does most of the heavy lifting with respect to figuring out the format. So instead of the Accept header check, you could just use that.

    If you were going to render multiple formats, I’d probably update the filter to not look for a ‘jsonify’ static field and instead make it more generic. Perhaps a static formats = [show: ['json', 'xml'], list: ['json']] that would let you tweak formats per action.

  5. Pingback: Questa settimana in Grails (2012-42) - luca-canducci.com - Il blog di Luca Canducci: notizie, tips e nuove tecnologie dal mondo dell’IT.

  6. Pingback: An Army of Solipsists » Blog Archive » This Week in Grails (2012-42)

  7. Pingback: This Week in Grails (2012-42) - Grails Info

  8. static says:

    Anyone get this working with multi-formats?

Leave a Reply

Your email address will not be published. Required fields are marked *

*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>