Skip to content

Provides alternate JSON (de)serialization for Grails using Google's Gson library

License

Notifications You must be signed in to change notification settings

robfletcher/grails-gson

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Grails Gson plugin

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 'org.grails.plugins:gson:1.1.4' 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.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.
	}
}

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

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" }
	]
}

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.

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.

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.

See the Gson documentation on custom serialization and deserialization for more information on how to write TypeAdapter implementations.

Unit test support

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.

Scaffolding RESTful controllers

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.

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.

Configuration

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.

Version history

  • Fixes a problem in unit tests with request.GSON = x where x is anything other than a String.
  • Fixes a bug where the plugin breaks domainClass.properties = x where x is anything other than a JsonObject.
  • 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.

About

Provides alternate JSON (de)serialization for Grails using Google's Gson library

Resources

License

Stars

Watchers

Forks

Packages

No packages published