Search

Dark theme | Light theme

July 11, 2014

Grails Goodness: Using Converter Named Configurations with Default Renderers

Sometimes we want to support in our RESTful API a different level of detail in the output for the same resource. For example a default output with the basic fields and a more detailed output with all fields for a resource. The client of our API can then choose if the default or detailed output is needed. One of the ways to implement this in Grails is using converter named configurations.

Grails converters, like JSON and XML, support named configurations. First we need to register a named configuration with the converter. Then we can invoke the use method of the converter with the name of the configuration and a closure with statements to generate output. The code in the closure is executed in the context of the named configuration.

The default renderers in Grails, for example DefaultJsonRenderer, have a property namedConfiguration. The renderer will use the named configuration if the property is set to render the output in the context of the configured named configuration. Let's configure the appropriate renderers and register named configurations to support the named configuration in the default renderers.

In our example we have a User resource with some properties. We want to support short and details output where different properties are included in the resulting format.

First we have our resource:

// File: grails-app/domain/com/mrhaki/grails/User.groovy
package com.mrhaki.grails

import grails.rest.*

// Set URI for resource to /users.
// The first format in formats is the default
// format. So we could use short if we wanted
// the short compact format as default for this 
// resources.
@Resource(uri = '/users', formats = ['json', 'short', 'details'])
class User {

    String username
    String lastname
    String firstname
    String email

    static constraints = {
        email email: true
        lastname nullable: true
        firstname nullable: true
    }

}

Next we register two named configurations for this resource in our Bootstrap:

// File: grails-app/conf/Bootstrap.groovy
class Bootstrap {

    def init = { servletContext ->
        ...
        JSON.createNamedConfig('details') {
            it.registerObjectMarshaller(User) { User user ->
                final String fullname = [user.firstname, user.lastname].join(' ')
                final userMap = [
                        id      : user.id,
                        username: user.username,
                        email   : user.email,
                ]
                if (fullname) {
                    userMap.fullname = fullname
                }
                userMap
            }
            // Add for other resources a marshaller within
            // named configuration.
        }

        JSON.createNamedConfig('short') {
            it.registerObjectMarshaller(User) { User user ->
                final userMap = [
                        id      : user.id,
                        username: user.username
                ]
                userMap
            }
            // Add for other resources a marshaller within
            // named configuration.
        }    
        ...
    }
...
}

Now we must register custom renderers for the User resource as Spring components in resources:

// File: grails-app/conf/spring/resources.groovy
import com.mrhaki.grails.User
import grails.rest.render.json.JsonRenderer
import org.codehaus.groovy.grails.web.mime.MimeType

beans = {
    // Register JSON renderer for User resource with detailed output.
    userDetailsRenderer(JsonRenderer, User) {
        // Grails will compare the name of the MimeType
        // to determine which renderer to use. So we 
        // use our own custom name here. 
        // The second argument, 'details', specifies the
        // supported extension. We can now use
        // the request parameter format=details to use
        // this renderer for the User resource.
        mimeTypes = [new MimeType('application/vnd.com.mrhaki.grails.details+json', 'details')]
        
        // Here we specify the named configuration
        // that must be used by an instance
        // of this renderer. See Bootstrap.groovy
        // for available registered named configuration.
        namedConfiguration = 'details'
    }

    // Register second JSON renderer for User resource with compact output.
    userShortRenderer(JsonRenderer, User) {
        mimeTypes = [new MimeType('application/vnd.com.mrhaki.grails.short+json', 'short')]

        // Named configuration is different for short
        // renderer compared to details renderer.
        namedConfiguration = 'short'
    }

    // Default JSON renderer as fallback.
    userRenderer(JsonRenderer, User) {
        mimeTypes = [new MimeType('application/json', 'json')]
    }

}

We have defined some new mime types in grails-app/conf/spring/resources.groovy, but we must also add them to our grails-app/conf/Config.groovy file:

// File: grails-app/conf/spring/resources.groovy
...
grails.mime.types = [
    ...
    short   : ['application/vnd.com.mrhaki.grails.short+json', 'application/json'],
    details : ['application/vnd.com.mrhaki.grails.details+json', 'application/json'],
]
...

Our application is now ready and configured. We mostly rely on Grails content negotiation to get the correct renderer for generating our output if we request a resource. Grails content negotiation can use the value of the request parameter format to find the correct mime type and then the correct renderer. Grails also can check the Accept request header or the URI extension, but for our RESTful API we want to use the format request parameter.

If we invoke our resource with different formats we see the following results:

$ curl -X GET http://localhost:8080/rest-sample/users/1
{
    "class": "com.mrhaki.grails.User",
    "id": 1,
    "email": "hubert@mrhaki.com",
    "firstname": "Hubert",
    "lastname": "Klein Ikkink",
    "username": "mrhaki"
}
$ curl -X GET http://localhost:8080/rest-sample/users/1?format=short
{
    "id": 1,
    "username": "mrhaki"
}
$ curl -X GET http://localhost:8080/rest-sample/users/1?format=details
{
    "id": 1,
    "username": "mrhaki",
    "email": "hubert@mrhaki.com",
    "fullname": "Hubert Klein Ikkink"
}

Code written with Grails 2.4.2.