Provides alternate JSON (de)serialization for Grails using Google's Gson library
This plugin provides alternate JSON (de)serialization for Grails using Google's Gson library.
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.
Add compile 'org.grails.plugins:gson:1.1.4'
to grails-app/conf/BuildConfig.groovy
.
The plugin provides a Grails converter implementation so that you can replace usage of the existing grails.converters.JSON
class with grails.plugin.gson.converters.GSON
. For example:
import grails.plugin.gson.converters.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.
}
}
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)
}
}
By default the plugin will automatically serialize any Hibernate proxies it encounters when serializing an object graph to JSON, resolving any uninitialized proxies along the way. This means by default you get a full, deep object graph at the potential cost of additional SQL queries. There are two config flags to control this behavior in your Config.groovy. If you set grails.converters.gson.resolveProxies
to false
then only initialized proxies are serialized – therefore no additional queries are performed. If you set grails.converters.gson.serializeProxies
to false
then no proxies are serialized at all meaning your JSON will only contain a shallow object graph.
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" }
]
}
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.
The deserializer is pre-configured to handle:
If a JSON object contains an id
property then it will use GORM to retrieve an existing instance, otherwise it creates a new one.
The deserializer respects the bindable
constraint so any properties that are blacklisted from binding are ignored. Any JSON properties that do not correspond to persistent properties on the domain class are ignored. Any other properties of the JSON object are bound to the domain instance.
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.
{
"name": "Alex",
"age": 3,
"pets": [
{"name": "Goldie", "species": "Goldfish"},
{"name": "Dottie", "species": "Goldfish"}
]
}
{
"id": 1,
"pets": [
{"name": "Goldie", "species": "Goldfish"},
{"name": "Dottie", "species": "Goldfish"}
]
}
{
"name": "Alex",
"age": 3,
"pets": [
{"id": 1},
{"id": 2}
]
}
{
"id": 1,
"pets": [
{"id": 1, "name": "Goldie"},
{"id": 2, "name": "Dottie"}
]
}
The gsonBuilder
factory bean provided by the plugin will automatically register any Spring beans that implement the TypeAdapterFactory
interface.
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.
See the Gson documentation on custom serialization and deserialization for more information on how to write TypeAdapter
implementations.
The plugin provides a test mixin. Simply add @TestMixin(GsonUnitTestMixin)
to test or spec classes. The mixin registers beans in the mock application context that are required for the GSON converter class to work properly. It also ensures that binding and rendering works with @Mock domain classes just as it does in a real running application.
In addition the mixin adds:
GSON
property on HttpServletResponse for convenience in making assertions in controller tests.GSON
property on HttpServletResponse that accepts either a JsonElement or a JSON string.The GSON plugin includes a scaffolding template for RESTful controllers designed to work with Grails' resource style URL mappings. To install the template run:
grails install-gson-templates
This will overwrite any existing file in src/templates/scaffoldng/Controller.groovy
. You can then generate RESTful controllers that use GSON using the normal dynamic or static scaffolding capabilities.
When trying to bind an entire object graph you need to be mindful of the way GORM cascades persistence changes.
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.
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.
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.
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.
The plugin supports a few configurable options. Where equivalent configuration applies to the standard Grails JSON converter then the same configuration can be used for the GSON converter.
grails.converters.gson.serializeProxies if set to true
then any Hibernate proxies are traversed when serializing entities to JSON. Defaults to true
. If set to false
any n-to-one proxies are serialized as just their identifier and any n-to-many proxies are omitted altogether.
grails.converters.gson.resolveProxies if set to true
then any Hibernate proxies are initialized when serializing entities to JSON. Defaults to true
. If set to false
only proxies that are already initialized get serialized to JSON. This flag has no effect if grails.converters.gson.serializeProxies
is set to false
as proxies will not be traversed anyway.
grails.converters.gson.pretty.print if set to true
then serialization will output pretty-printed JSON. Defaults to grails.converters.default.pretty.print
or false
. See GsonBuilder.setPrettyPrinting.
grails.converters.gson.domain.include.class if set to true
then serialization will include domain class names. Defaults to grails.converters.domain.include.class
or false
.
grails.converters.gson.domain.include.version if set to true
then serialization will include entity version. Defaults to grails.converters.domain.include.version
or false
.
grails.converters.gson.serializeNulls if set to true
then null
properties are included in serialized JSON, otherwise they are omitted. Defaults to false
. See GsonBuilder.serializeNulls
.
grails.converters.gson.complexMapKeySerialization if set to true
then object map keys are serialized as JSON objects, otherwise their toString
method is used. Defaults to false
. See GsonBuilder.enableComplexMapKeySerialization
.
grails.converters.gson.escapeHtmlChars if set to true
then HTML characters are escaped in serialized output. Defaults to true
. See GsonBuilder.disableHtmlEscaping
.
grails.converters.gson.generateNonExecutableJson if set to true
then serialized output is prepended with an escape string to prevent execution as JavaScript. Defaults to false
. See GsonBuilder.generateNonExecutableJson
.
grails.converters.gson.serializeSpecialFloatingPointValues if set to true
then serialization will not throw an exception if it encounters a special long value such as NaN. Defaults to false
. See GsonBuilder.serializeSpecialFloatingPointValues
.
grails.converters.gson.longSerializationPolicy specifies how long values are serialized. Defaults to LongSerializationPolicy.DEFAULT
. See GsonBuilder.setLongSerializationPolicy
.
grails.converters.gson.fieldNamingPolicy specifies how field names are serialized. Defaults to FieldNamingPolicy.IDENTITY
. See GsonBuilder.setFieldNamingStrategy
.
grails.converters.gson.datePattern specifies the pattern used to format java.util.Date
objects in serialized output. If this is set then dateStyle
and timeStyle
are ignored. See GsonBuilder.setDateFormat(String)
.
grails.converters.gson.dateStyle and grails.converters.gson.timeStyle specify the style used to format java.util.Date
objects in serialized output. See GsonBuilder.setDateFormat(int, int)
. The values should be one of the int
constants - SHORT
, MEDIUM
, LONG
or FULL
- from java.text.DateFormat
. Note that Gson does not have a way to specify a locale for the format so Locale.US
is always used. For more control over the format use grails.converters.gson.datePattern or register a custom TypeAdapterFactory
.
request.GSON = x
where x
is anything other than a String
.domainClass.properties = x
where x
is anything other than a JsonObject
.GsonUnitTestMixin
for unit test support.Bugfix release.
Initial release.