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:
- domain classes
- domain associations
- Set, List and Map associations
- embedded properties
- collections of basic types
- arbitrary depth object graphs
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:
- a
GSON
property on HttpServletResponse for convenience in making assertions in controller tests. - a writable
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 totrue
. If set tofalse
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 totrue
. If set tofalse
only proxies that are already initialized get serialized to JSON. This flag has no effect ifgrails.converters.gson.serializeProxies
is set tofalse
as proxies will not be traversed anyway. -
grails.converters.gson.pretty.print if set to
true
then serialization will output pretty-printed JSON. Defaults tograils.converters.default.pretty.print
orfalse
. See GsonBuilder.setPrettyPrinting. -
grails.converters.gson.domain.include.class if set to
true
then serialization will include domain class names. Defaults tograils.converters.domain.include.class
orfalse
. -
grails.converters.gson.domain.include.version if set to
true
then serialization will include entity version. Defaults tograils.converters.domain.include.version
orfalse
. -
grails.converters.gson.serializeNulls if set to
true
thennull
properties are included in serialized JSON, otherwise they are omitted. Defaults tofalse
. SeeGsonBuilder.serializeNulls
. -
grails.converters.gson.complexMapKeySerialization if set to
true
then object map keys are serialized as JSON objects, otherwise theirtoString
method is used. Defaults tofalse
. SeeGsonBuilder.enableComplexMapKeySerialization
. -
grails.converters.gson.escapeHtmlChars if set to
true
then HTML characters are escaped in serialized output. Defaults totrue
. SeeGsonBuilder.disableHtmlEscaping
. -
grails.converters.gson.generateNonExecutableJson if set to
true
then serialized output is prepended with an escape string to prevent execution as JavaScript. Defaults tofalse
. SeeGsonBuilder.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 tofalse
. SeeGsonBuilder.serializeSpecialFloatingPointValues
. -
grails.converters.gson.longSerializationPolicy specifies how long values are serialized. Defaults to
LongSerializationPolicy.DEFAULT
. SeeGsonBuilder.setLongSerializationPolicy
. -
grails.converters.gson.fieldNamingPolicy specifies how field names are serialized. Defaults to
FieldNamingPolicy.IDENTITY
. SeeGsonBuilder.setFieldNamingStrategy
. -
grails.converters.gson.datePattern specifies the pattern used to format
java.util.Date
objects in serialized output. If this is set thendateStyle
andtimeStyle
are ignored. SeeGsonBuilder.setDateFormat(String)
. -
grails.converters.gson.dateStyle and grails.converters.gson.timeStyle specify the style used to format
java.util.Date
objects in serialized output. SeeGsonBuilder.setDateFormat(int, int)
. The values should be one of theint
constants -SHORT
,MEDIUM
,LONG
orFULL
- fromjava.text.DateFormat
. Note that Gson does not have a way to specify a locale for the format soLocale.US
is always used. For more control over the format use grails.converters.gson.datePattern or register a customTypeAdapterFactory
.
- Fixes a problem in unit tests with
request.GSON = x
wherex
is anything other than aString
.
- Fixes a bug where the plugin breaks
domainClass.properties = x
wherex
is anything other than aJsonObject
.
- Adds
GsonUnitTestMixin
for unit test support.
- Fixes a compilation problem with scaffolded controllers that use the RESTful controller template
- Introduces various configuration options
- Adds RESTful controller template
Bugfix release.
- Fixes deserialization of bi-directional relationships so tbat the domain instances can be save successfully.
- Ignores unknown properties in JSON rather than throwing an exception (contributed by @gavinhogan).
Initial release.