The development of this plugin has been inspired by this blog post and a need to use custom marshallers - mostly by REST services whose responses must fulfill already defined formats.
- Added a new include option (allow only specific properties - issue #12)
- Added marshalling context as an additional argument to virtual property closure. See the test for the example of usage
- Minor fixes and documentation updates
Creating and Registering Custom Marshallers
Grails Converter plugin shipping within the Grails installation provides already all pieces needed to register and use a custom XML or JSON marshaller, but the way how to achieve it is (1) rather undocumented and (2) requires explicit registrations somewhere in application code (usually in BootStrap file). This plugin adds to application XmlMarshaller and JsonMarshaller artefact types allowing transparent registration of custom XML and/or JSON marshallers defined by the appropriate artefacts.
Configuring Domain Class Marshalling
As far as marshalling of domain instances is concerned, converters plugin provides and registers two built in marshaller implementation per format. These are DomainClassJSON(XML)Marshaller and DeepDomainClassJSON(XML)Marshaller. The problem is the fact that there is no mechanism to somehow customize output of these marshallers. This plugin adds a marshaller configuration option to each domain class allowing to customze output produces by DomainClass marshaller.
Feel free to check unit tests for detailed examples of usage
- For apps using Grails version <= 2.2.x -> compile ":marshallers:0.4"
- For apps using Grails version >= 2.3.x -> compile ":marshallers:0.6"
Let's suppose that we have a domain class:
class A {
String foo
String bar
}
and we need to serialize the fields as XML attributes.
here is the marshaller that does this for us:
import org.codehaus.groovy.grails.web.converters.marshaller.ObjectMarshaller
class AXmlMarshaller implements ObjectMarshaller<XML> {
boolean supports(Object object) {
A.isAssignableFrom(object.class)
}
void marshalObject(obj, xml) {
xml.attribute("foo", obj.foo)
xml.attribute("bar", obj.bar)
}
}
and it will be registered at the application startup. Note that this is a standard Grails artifact - thus dependency injection works as expected.
A nice feature of the actual converter architecture is support for named configurations that can be hierarchical as well. Imagine that you need to serialize in some cases instances of A with "foo" content only. Let us register the appropriate marshaller in the configuration entitled 'restricted':
import org.codehaus.groovy.grails.web.converters.marshaller.ObjectMarshaller
class AXmlMarshaller implements ObjectMarshaller<XML> {
static configuration = "restricted"
boolean supports(Object object) {
A.isAssignableFrom(object.class)
}
void marshalObject(obj, xml) {
xml.attribute("foo", obj.foo)
}
}
The static property configuration can contain one particular converter configuration name or a list of names where the marshaller should be registered to. Further, we need specify the configuration hierarchy in application Config:
grails.plugins.marshallers.xml = {
restricted {
evenMoreRestricted {
}
}
}
We always have one default system configuration that is the parent of the named configurations on the first level. If a given configuration does not define a marshaller for a given class, it will be searched in its parent configuration until the system configuration is reached.
Now, when we need to serialize instances of A restrictively, we would say:
import import grails.converters.XML
def get_restricted = {
def a = A.get(params.id)
XML.use('restricted') {
render a as XML
}
}
If your marshallers are simple and/or you do not want to have them as artifacts in the system, you can register them in-place while defining named configurations:
grails.plugins.marshallers.xml = {
register A { obj, xml ->
xml.attribute("foo", obj.foo)
xml.attribute("bar", obj.bar)
}
restricted {
register A { obj, xml ->
xml.attribute("foo", obj.foo)
}
evenMoreRestricted {
}
}
}
Two more registration statements are available:
- register(Class class, Closure elementNameClosure, Closure contentClosure) - needed in a case when XML element name should be custom, e.g.
register A { obj -> "custom" } { obj, xml ->
xml.attribute("foo", obj.foo)
xml.attribute("bar", obj.bar)
}
-
register(Class class) - when registering marshaller whose logic is in class and the class naming does not follow marshaller artifact convention, e.g.:
register CustomAXMLSerializer
Along to developing and registering a custom marshaller, the way how a domain class instance is marshalled can be specified within the domain class itself - specifying the marshalling configuration(s). Maybe the best way to ilustrate the machanism is with a small example
Let's assume that you have the following domain classes which has to be serialized
class Author {
String name
Date dob
static hasMany = [books: Book]
}
class Book {
String isbn
String name
}
and we have the following requirements
- dob and isbn fields has to be serialized as attributes
- books belonging to an author has to be serialized as children of author xml element
- identifier , class and version information should be suppressed
Marshaling configuration for such a case is specified as a static closure of each class
class Author {
static marshalling={
shouldOutputIdentifier false
shouldOutputVersion false
shouldOutputClass false
attribute 'dob'
deep 'books'
}
String name
Date dob
List books
}
class Book {
static marshalling={
shouldOutputIdentifier false
shouldOutputVersion false
shouldOutputClass false
elementName 'my-book'
attribute 'isbn'
}
String isbn
String name
}
When configuration is defined as above the following snippet of code would perform actual serialization
Author author=Author.findByName('Jonathan Franzen')
render author as XML
Producing the following hypothetical XML output
<?xml version="1.0" encoding="UTF-8"?>
<author dob="Fri Aug 17 00:00:00 CET 1959">
<name>Jonathan Franzen</name>
<books>
<my-book isbn="1111">
<name>The Twenty-Seventh City</name>
<my-book>
<my-book isbn="2222">
<name>Freedom</name>
<my-book>
</books>
</author>
while
Author author=Author.findByName('Jonathan Franzen')
render author as JSON
would produce the following JSON output
{
"books":[
{"isbn":"2222","name":"Freedom"},
{"isbn":"1111","name":"The Twenty-Seventh City"}
],
"dob":"1959-08-17T00:00:00Z",
"name":"Jonathan Franzen"
}
Within the marshalling configuration closure there are several configuration options possible.
- shouldOutputIdentifier when true will suppress serialization of domain object identifier (json,xml)
- shouldOutputClass whether class information should be serialized or not (json,xml)
- shouldOutputVersion whether version information should be serialized or not (json,xml)
- identifier is a comma separated list of fields which uniquely identifies a domain object in case database id is not sufficient. (xml)
- elementName configures a custom domain object element name which should be used instead of default one (xml)
- attribute is a comma separated list of field names which will be serialized as attributes of domain object element (xml)
- deep is a comma separated list of field names. If a field representing one-to-many relation is marked as deep, all contained data of related objects will be serialized (json,xml)
- serializer is a configuration option which allows us to define closures with custom serialization behavior. This configuration options allows us to customize serialization output for existing property (json,xml)
- virtual unlike serializer which will create completely new property
- ignore is a comma separated list of properties which should be ignored during serialization process (json,xml)
- include is an exclusive comma separated list of properties which will be included, meaning properties not listed will not be included (except for those specified using the should* options, or in case of XML, those listed as attribute or identifier). The include and ignore options are mutually exclusive - if both are defined, include takes priority (json,xml)
###Named and marshaller specific configuration
Example above specifies configuration will be applied both to xml and json marshallers. If we would like to specify marshaller specific configuration it can be done as follows
class Author {
static marshalling={
xml {
//some xml specific configuration
}
json{
//some json specific configuration
}
}
String name
Date dob
List books
}
Furthermore, it is possible to specify named configurations
class Author {
static marshalling={
xml {
namedConfiguration{
//some xml specific configuration
}
}
json{
someOtherNamedConfig{
//some json specific configuration
}
}
}
String name
Date dob
List books
}
which will be than used as follows
Author author=Author.findByName('Jonathan Franzen')
XML.use('someNamedConfiguration'){
render author as XML
}
###Example serializer and virtual use
class Author {
static marshalling={
specialReport{
serializer{ // customize the name output to all caps for our 'special report'
name { value, json -> json.value("${value.name.toUpperCase()}") }
//it is also possible to pass the marshalling context map as a parameter of virtual property
//value would be put to map as GenericDomainClassJSONMarshaller.marshallingContext.myCtxKey="context value"
ctxName { value, json,ctx -> json.value(ctx.myCtxKey) }
}
virtual{ // add a virtual property, in this case a date/time stamp
time { value, json -> json.value("${new Date()}") }
}
}
}
String name
Date dob
List books
}