There are many cases to represent objects as JSON in HTTP response. One of this is building the API. Grails has build-in converters both for XML and JSON that get all properties from the object to serizalize as string.
What is wrong
with Grails JSON converters used out-of-the-box for building API response?
For domain classes, redundant properties (in API use case) are considred as reslt of serialization. This causes mess in response:
{
"class":"com.selly.domain.entity.Product",
"id":1,
"category":[
{
"class":"ProductCategory",
"id":1
}
],
"data":{
"class":"ProductData",
"id":1
},
"workspace":1
}
As you can see, the class
property is also included, some fields are not deeply included (saying nothing of deep converters).
Of course there is possible to customize object conversion by Marshallers in
easy way by defining them as a closures by calling JSON.registerObjectMarshaller(Object, Closure { ... })
I have prepared the Marshaller that
produces strategy in convenient way (like in jms/serializer-bundle Symfony Bundle). You can use it in two strategies:
- Exclusion policy
all fields are considered during conversion excepts explicit defined
- Inclusion policy
only explicit defined fields are considered during conversion
There are many plugins for Grails, but in my opinion installing them are additional overhead.
Imagine, that you define the object serialization specification like:
JSON.registerObjectMarshaller(ProductCategory,
DomainClassMarshaller.createExcludeMarshaller(ProductCategory,
["product", "parent"]
))
or:
JSON.registerObjectMarshaller(Product,
DomainClassMarshaller.createIncludeMarshaller(Product,
["id", "name", "description"]
))
The use case is just return object in controller’s response as JSON object:
def sampleAction(Long id) {
def item = Product.getById(id)
render item as JSON
}
You can also consider custom serialization strategy in context of use case, for example all users should know product name and description, but not sales statistics included in the entity.
There is possibility to create custom configurations in Grails serialization:
JSON.createNamedConfig("forCustomers") {
JSON.registerObjectMarshaller(ProductCategory, ...)
// inclusion policy, only id, name and description
}
And use case:
def sampleCustomerAction(Long id) {
def item = Product.getById(id)
JSON.use("forCustomers") {
render item as JSON
}
}
Finally I include my simple class to serialize domain object with inclusion and exclusion policy. This uses DefaultGrailsDomainClass to obtain only persistent fields.
package com.selly.util.converter.json
import org.codehaus.groovy.grails.commons.DefaultGrailsDomainClass
/**
* This class provides the inclusion and exclusion policy
* for Marshallers.
*
* Usage for exclusion policy:
*
* JSON.registerObjectMarshaller(MyDomainClass, DomainClassMarshaller.createExcludeMarshaller(MyDomainClass, ["excluedField1", "excluedField2"]))
*
*
* Usage for inclusion policy:
*
* JSON.registerObjectMarshaller(MyDomainClass, DomainClassMarshaller.createIncludeMarshaller(MyDomainClass, ["id", "name", "description"]))
*
*
* Usage in controller:
*
* def sampleAction(Long id) {
* def item = Product.getById(id)
* response item as JSON
* }
*
*
* Create custom configuration:
*
* JSON.createNamedConfig("forAdmin") {
* JSON.registerObjectMarshaller(MyDomainClass, DomainClassMarshaller.createIncludeMarshaller(MyDomainClass, ["id", "name", "description", "stats"]))
* }
*
* And controller:
*
* def sampleAction(Long id) {
* def item = Product.getById(id)
* JSON.use("forAdmin") {
* response item as JSON
* }
* }
*
*
* @author Piotr 'Athlan' Pelczar
*
*/
class DomainClassMarshaller {
public static List globalRestrictedFields = ['class']
public static Closure createIncludeMarshaller(Class clazz, List fieldsToInclude) {
return { domainItem ->
DefaultGrailsDomainClass domain = new DefaultGrailsDomainClass(clazz)
def results = [:]
domain.persistentProperties.each { field ->
if(!(field.name in globalRestrictedFields) && (field.name in fieldsToInclude))
results[field.name] = domainItem[field.name]
}
return results
}
}
public static Closure createExcludeMarshaller(Class clazz, List fieldsToExclude = []) {
return { domainItem ->
DefaultGrailsDomainClass domain = new DefaultGrailsDomainClass(clazz)
def results = [:]
domain.persistentProperties.each { field ->
if(!(field.name in globalRestrictedFields) && !(field.name in fieldsToExclude))
results[field.name] = domainItem[field.name]
}
return results
}
}
}
You can register the marshallers in BootStrap.groovy file, or :
JSON.registerObjectMarshaller(Product, DomainClassMarshaller.createExcludeMarshaller(Product))
JSON.registerObjectMarshaller(ProductData, DomainClassMarshaller.createExcludeMarshaller(ProductData))
JSON.registerObjectMarshaller(ProductCategory, DomainClassMarshaller.createExcludeMarshaller(ProductCategory, ["product"]))
JSON.registerObjectMarshaller(ProductCategoryData, DomainClassMarshaller.createExcludeMarshaller(ProductCategoryData, ["product"]))
JSON.createNamedConfig("custom") {
JSON.registerObjectMarshaller(ProductCategory, DomainClassMarshaller.createExcludeMarshaller(ProductCategory))
}
Now, the response is clean and fully controlled.