Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Provides alternate JSON (de)serialization for Grails using Google's Gson library
Groovy Shell
tag: 1.0

Fetching latest commit…

Cannot retrieve the latest commit at this time

Failed to load latest commit information.
grails-app
scripts
src/groovy/grails/plugin/gson
test
wrapper
.gitignore
.travis.yml
GsonGrailsPlugin.groovy
LICENSE
application.properties
grailsw
grailsw.bat
readme.md

readme.md

Build Status

This plugin provides alternate JSON (de)serialization for Grails using Google's Gson library.

Rationale

Grails' JSON deserialization has some limitations. Specifically it doesn't work with nested object graphs. This means you can't bind a JSON data structure to a GORM domain class and have it populate associations, embedded properties, etc.

There is a JIRA open for this issue but since it's easy to provide an alternative with Gson I thought a plugin was worthwhile.

Installation

Add compile ':gson:1.0' to grails-app/conf/BuildConfig.groovy.

Usage

Using Grails converters

The plugin provides a Grails converter implementation so that you can replace usage of the existing grails.converters.JSON class with grails.plugin.gson.GSON. For example:

import grails.plugin.gson.GSON

class PersonController {
    def list() {
        render Person.list(params) as GSON
    }

    def save() {
        def personInstance = new Person(request.GSON)
        // ... etc.
    }

    def update() {
        def personInstance = Person.get(params.id)
        personInstance.properties = request.GSON
        // ... etc.
    }
}

Using Gson directly

The plugin provides a GsonBuilder factory bean that you can inject into your components. This is pre-configured to register type handlers for domain classes so you don't need to worry about doing so unless you need to override specific behaviour.

class PersonController {
    def gsonBuilder

    def list() {
        def gson = gsonBuilder.create()
        def personInstances = Person.list(params)
        render contentType: 'application/json', text: gson.toJson(personInstances)
    }

    def save() {
        def gson = gsonBuilder.create()
        def personInstance = gson.fromJson(request.reader, Person)
        if (personInstance.save()) {
            // ... etc.
    }

    def update() {
        def gson = gsonBuilder.create()
        // because the incoming JSON contains an id this will read the Person
        // from the database and update it!
        def personInstance = gson.fromJson(request.reader, Person)
    }
}

Serialization

The plugin will automatically resolve any Hibernate proxies it encounters when serializing an object graph to JSON.

If an object graph contains bi-directional relationships they will only be traversed once but in either direction. For example if you have the following domain classes:

class Artist {
    String name
    static hasMany = [albums: Album]
}

class Album {
    String title
    static belongsTo = [artist: Artist]
}

Instances of Album will get serialized to JSON as:

{
    "id": 2,
    "title": "The Rise and Fall of Ziggy Stardust and the Spiders From Mars",
    "artist": {
        "id": 1,
        "name": "David Bowie"
    }
}

And instances of Artist will get serialized to JSON as:

{
    "id": 1,
    "name": "David Bowie",
    "albums": [
        { "id": 1, "title": "Hunky Dory" },
        { "id": 2, "title": "The Rise and Fall of Ziggy Stardust and the Spiders From Mars" },
        { "id": 3, "title": "Low" }
    ]
}

Deserialization

The plugin registers a JsonDeserializer that handles conversion of JSON to Grails domain objects. It will handle deserialization at any level of a JSON object graph so embedded objects, relationships and persistent collections can all be modified when binding to the top level domain object instance.

If a JSON object contains an id property then it will use GORM to retrieve an existing instance, otherwise it creates a new one.

Any other properties of the JSON object are bound to the domain instance. The deserializer respects the bindable constraint so any properties that are blacklisted from binding are ignored.

Deserialization examples

Let's say you have a domain classes Child and Pet like this:

class Child {
    String name
    int age
    static hasMany = [pets: Pet]
}

class Pet {
    String name
    String species
    static belongsTo = [child: Child]
}

This can be deserialized in a number of ways.

To create a new Child instance with associated Pet instances

{
    "name": "Alex",
    "age": 3,
    "pets": [
        {"name": "Goldie", "species": "Goldfish"},
        {"name": "Dottie", "species": "Goldfish"}
    ]
}

To bind new Pet instances to an existing Child

{
    "id": 1,
    "pets": [
        {"name": "Goldie", "species": "Goldfish"},
        {"name": "Dottie", "species": "Goldfish"}
    ]
}

To bind existing Pet instances to a new Child

{
    "name": "Alex",
    "age": 3,
    "pets": [
        {"id": 1},
        {"id": 2}
    ]
}

To update the name of existing Pet instances without changing their species

{
    "id": 1,
    "pets": [
        {"id": 1, "name": "Goldie"},
        {"id": 2, "name": "Dottie"}
    ]
}

Registering additional type adapters

The gsonBuilder factory bean provided by the plugin will automatically register any Spring beans that implement the TypeAdapterFactory interface.

Example

To register support for serializing and deserializing org.joda.time.LocalDate properties you would define a TypeAdapter implementation:

class LocalDateAdapter extends TypeAdapter<LocalDate> {

    private final formatter = ISODateTimeFormat.date()

    void write(JsonWriter jsonWriter, LocalDateTime t) {
        jsonWriter.value(t.toString(formatter))
    }

    LocalDateTime read(JsonReader jsonReader) {
        formatter.parseLocalDate(jsonReader.nextString())
    }
}

Then create a TypeAdapterFactory:

class LocalDateAdapterFactory implements TypeAdapterFactory {
    TypeAdapter create(Gson gson, TypeToken type) {
        type.rawType == LocalDate ? new LocalDateAdapter() : null
    }
}

Finally register the TypeAdapterFactory in grails-app/conf/spring/resources.groovy:

beans {
    localDateAdapterFactory(LocalDateAdapterFactory)
}

The plugin will then automatically use it.

Compatibility

The plugin's Gson deserializer works with:

  • domain classes
  • domain associations
  • Set, List and Map associations
  • embedded properties
  • collections of basic types
  • arbitrary depth object graphs

Gotchas

When trying to bind an entire object graph you need to be mindful of the way GORM cascades persistence changes.

Cascading updates

Even though you can bind nested domain relationships there need to be cascade rules in place so that they will save.

In the examples above the Pet domain class must declare that it belongsTo Child (or Child must declare that updates cascade to pets). Otherwise the data will bind but when you save the Child instance the changes to any nested Pet instances will not be persisted.

Cascading saves

Likewise if you are trying to create an entire object graph at once the correct cascade rules need to be present.

If Pet declares belongsTo = [child: Child] everything should work as Grails will apply cascade all by default. However if Pet declares belongsTo = Child then Child needs to override the default cascade save-update so that new Pet instances are created properly.

See the Grails documentation on the cascade mapping for more information.

Circular references

Gson does not support serializing object graphs with circular references and a StackOverflowException will be thrown if you try. The plugin protects against circular references caused by bi-directional relationships in GORM domain classes but any other circular reference is likely to cause a problem when serialized. If your domain model contains such relationships you will need to register additional TypeAdapter implementations for the classes involved.

Parameter parsing

In general it is possible to use the Gson plugn alongside Grails' built in JSON support. The only thing the plugin overrides in the parsing of a JSON request body into a parameter map.

This is only done when you set parseRequest: true in URLMappings or use a resource style mapping. See the Grails documentation on REST services for more information.

The plugin's parsing is compatible with that done by the default JSON handler so you should see no difference in the result.

Something went wrong with that request. Please try again.