6
0
0

Make Grails Object Marshalling More RESTful

Tareq Abedrabboviacafebabe
August 03, 2009

If you use Grails to develop RESTful services, then you're probably familiar with the different ways of producing XML and JSON representations for your objects. One particularly convenient option is to make use of the provided converters, along with the render method.



Taking the stereotypical book store application as an example, the following code in BookController:

render book as JSON
will produce something like the following, assuming that we are using the standard JSON converter (grails.converters.JSON):

{
    "class": "Book",
    "id": "1",
    "author": {
        "class":"Author",
        "id": 1
    },
    "title":"Nineteen Eighty-Four"
}
I'm sure you figured out that Author:1 refers to George Orwell: born in 1903, author of other eminent works including "Animal Farm" and the guy who coined the term "Cold War" - but that's because you're smart. It certainly won't be as easy for a programmatic client to know as much about the author, and that's because the representation you're providing lacks an essential RESTful ingredient: connectedness. What the clients of your service would like to see are links (URIs) to nearby resources (i.e. the author of the book), to follow if they want to.

So ideally, we need a way to add the URIs of related resources to the representations we're returning, without changing the code that much. To illustrate the point, let's lay out the whole book store application first. The domain model is simply limited to Book and Author:

class Author {
    static hasMany = [books:Book]
    String name
}

class Book {
    static belongsTo = [author:Author]
    String title
}
I'll only implement the GET method in the controllers:

class AuthorController {
    def show = {
        def a = Author.read(params.id)
        if(a){
            render a as JSON
        }
        else{
            response.status = 404
            render "author ${params.id} not found"
        }
    }
}

class BookController {
    def show = {
        def a = Book.read(params.id)
        if(a){
            render a as JSON
        }
        else{
            response.status = 404
            render "book ${params.id} not found"
        }
    }
}
We also need to map the controllers as RESTful resources:

class UrlMappings {
    static mappings = {
        "/$controller/$id"{
            resource = {controller}
        }
        "/"(view:"/index")
        "500"(view:'/error')
    }
}
Because I mostly like the output of the JSON converter, I will define and attach to it a custom object marshaller that adds URIs to referenced objects. My new RestfulDomainClassMarshaller is based on the standard ..json.DomainClassMarshaller; We only need to redefine the asShortObject method. The marshaller also needs the base URI to function:

class RestfulDomainClassMarshaller extends DomainClassMarshaller {

    String baseUri

    protected void asShortObject(Object refObj, JSON json, GrailsDomainClassProperty idProperty, GrailsDomainClass referencedDomainClass)
    throws ConverterException {
        json.writer.object()
        json.writer.key("class").value(referencedDomainClass.name)
        json.writer.key("id").value(extractValue(refObj, idProperty))
        def uri = "$baseUri/${GrailsNameUtils.getPropertyName(referencedDomainClass.shortName)}/${extractValue(refObj, idProperty)}"
        json.writer.key("uri").value(uri)
        json.writer.endObject()
    }
}
The final step is to register the RESTful marshaller with the JSON converter in BootStrap.groovy. I'll get the base URI from the config parameter grails.serverURL, that's why the grailsApplication property is injected. A nice side effect of using grails.serverURL is that it's environment-scoped by default, which means that the URIs will continue to be valid in all the environments (if you configure them correctly that is).

class BootStrap {
    def grailsApplication

    def init = { servletContext ->
        def marshaller = new RestfulDomainClassMarshaller(baseUri:grailsApplication.config.grails.serverURL)
        JSON.registerObjectMarshaller(marshaller)

        // test data
        new Author(name:'George Orwell').
            addToBooks(new Book(title:'Animal Farm')).
            addToBooks(new Book(title:'Nineteen Eighty-Four')).save()
    }
    def destroy = {
    }
}
Now, GETing http://localhost:8080/RestfulBookStore/book/1 yields the following result:

{
    "class": "Book",
    "id": "1",
    "author": {
        "class": "Author",
        "id": 1,
        "uri": "http://localhost:8080/RestfulBookStore/author/1"
    },
    "title": "Nineteen Eighty-Four"
}
Similarly GETing http://localhost:8080/RestfulBookStore/author/1 gives:

{
    "class": "Author",
    "id": "1",
    "books": [
        {
            "class": "Book",
            "id": 1,
            "uri": "http://localhost:8080/RestfulBookStore/book/1"
        },
        {
            "class": "Book",
            "id": 2,
            "uri": "http://localhost:8080/RestfulBookStore/book/2"
        }
    ],
    "name": "George Orwell"
}
If you're using FireFox, try installing the nice JSONView extension, which pretty-prints JSON and makes the links clickable!

Download the code here.
Discussion

-