I like Grails for many reasons - it has all of the advantages of Rails plus the ability to make use of anything else running on the JVM. This means it is easier for Java developers to move to Grails and there is no need to re-implement working Java code. Pretty cool if you ask me. 

There is just on major problem with Grails - the JSON and XML support is a terrible. The good news it that you can fix it...of course it should be fixed in the framework but that is a different issue....

JSON Data binding fix for Grails

Have you seen this error when binding JSON in Grails?  Unable to Marshall to a Domain Object - no matching editors or conversion strategy found

Anyone that has tried to use JSON to communicate with a Grails backend has quickly discovered that the native JSON implementation does not support bindings for nested objects. They also discover that it is difficult to customize the JSON generated for an object. In my opinion, data binding and JSON customization are required minimum functionality - especially when writing Single-Page-Applicaitons (SPA's) backed by Grails. 

The problem with the native JSON handling in Grails is really just two defects (at least I call them defects)

The result from the native JSON Parser cannot be used to bind nested objects

As we have discovered, data binding only works with Maps - JSONElement implements Map, but JSONArray (representing nested objects for binding) is not supported for data binding. The only option you have is to convert the JSONArray to a map. In my case, I looked at the Map that would result from a traditional form submission and discovered that in order to create an identical map, the conversion needed to create nested maps as wells as individual (flat) keys representing the the data in the map - this means that some data is duplicated in the resulting map (just like it is in a traditional form submission) I only mention this so that you know it is intentional when you start playing with the code. NB: In this example, I use the Jackson parser, but I think the native JSON parser also work with this code.
First, add the following dependacies to your BuildConfig.groovy file.
1  runtime 'com.fasterxml.jackson.core:jackson-core:2.0.4'
2  runtime 'com.fasterxml.jackson.core:jackson-databind:2.0.4'
3  runtime 'com.fasterxml.jackson.core:jackson-annotations:2.0.4'
 If you are using the JAX-RS plugindownload the DomainObjectReader class and put it in your grails-app\providers directory and your done. Otherwise, you can use this code to create a valid, bindable paramaters map from your JSON.
type is the class of the domain object
entityStream contains the JSON
charset is the character encoding of the JSON in the entityStream
 1  /**
 2  * Construct domain object from json map obtained from entity stream.
 3  */
 4  protected Object readFromJson(Class type, InputStream entityStream, String charset) {
 5
6  def mapper = new ObjectMapper();
 7  def parsedJSON = mapper.readValue(entityStream, typeRef);
 8
9  Map map = new HashMap<>();
10
11  parsedJSON.entrySet().each {Map.Entry entry ->
12  if (List.isAssignableFrom(entry.getValue().getClass())) {
13
14  List values = (List) entry.getValue();
15  int limit = values.size()
16  for (int i = 0; i < limit; i++) {
17  final theValue = values.get(i)
18  map.put(entry.key + '[' + i + ']', theValue)
19
20  appendMapValues(map, theValue, entry.key + '[' + i + ']' )
21
22  }
23  } else {
24  map.put(entry.key, entry.value);
25  }
26  }
27
28  def result = type.metaClass.invokeConstructor(map)
29
30
31  // Workaround for http://jira.codehaus.org/browse/GRAILS-1984
32  if (!result.id) {
33  result.id = idFromMap(map)
34  }
35  result
36  }
37
38  private void appendMapValues(Map theMap, Object theValue, String prefix) {
39  if (Map.isAssignableFrom(theValue.getClass())) {
40
41  Map valueMap = (Map) theValue;
42
43  for (Map.Entry valueEntry : valueMap.entrySet()) {
44  theMap.put(prefix + '.' + valueEntry.key, valueEntry.value)
45  appendMapValues(theMap, valueEntry.value, prefix + '.' + valueEntry.key)
46  }
47  }
48  }

 

Complete customization of JSON is not supported

As you work with Grials and JSON, you will eventaully dislike something about the JSON generated by Grails. You will do a little searching on the web and discover that you can register custom ObjectMarshaller's with the JSON parser - happy with a solution you implement you ObjectMarshaller's despite that nagging feeling that this type of thing really should be handled in a more "data driven" way by Grails. All is well with your top level classes, but no matter what you do, the objects on the many side of the hasMany relationship always render the same JSON despite having registered a custom ObjectMarshaller.  You look through the Grails source code and discover that DomainClassMarshaller.asShortObject(...) ignored custom ObjectMarshaller's - it turns out that you cannot customize the JSON in all cases.
I wanted a solution that supported annotations to control how the JSON was rendered so I settled on an implementation that makes DomainClassMarshaller aware of two Jackson annotations to control how the JSON is rendered.
I started with the original DomainClassMarshaller and made changes to marshallObject to look at the @JsonIgnore annotations at the class level and property level to determine which properties to ignore. I then made changes to asShortObject to always include any property with the @JsonInclude annotation and to also omit the "class" property if necessary. 
I think this implementation is far from complete, but it should be enough to get you started with your own customizations if necessary. (please tell me about your changes)
To make use of this new Marshaller you must update Bootstrap.groovy with the following:
1  // Reference to Grails application.
2  def grailsApplication
3
4  def init = { servletContext ->
5
6  JSON.registerObjectMarshaller(new DomainClassMarshaller(true, grailsApplication))
7 
8  }
Then just place this class in the groovy source directory (download zip):
 1
2
3 import com.fasterxml.jackson.annotation.JsonIgnore
 4 import com.fasterxml.jackson.annotation.JsonIgnoreProperties
 5 import com.fasterxml.jackson.annotation.JsonInclude
 6 import grails.converters.JSON
 7 import org.codehaus.groovy.grails.support.proxy.DefaultProxyHandler
 8 import org.codehaus.groovy.grails.support.proxy.EntityProxyHandler
 9 import org.codehaus.groovy.grails.support.proxy.ProxyHandler
 10 import org.codehaus.groovy.grails.web.converters.ConverterUtil
 11 import org.codehaus.groovy.grails.web.converters.exceptions.ConverterException
 12 import org.codehaus.groovy.grails.web.converters.marshaller.ObjectMarshaller
 13 import org.codehaus.groovy.grails.web.json.JSONWriter
 14 import org.springframework.beans.BeanWrapper
 15 import org.springframework.beans.BeanWrapperImpl
 16
17 import java.lang.reflect.Field
 18
19 import org.codehaus.groovy.grails.commons.*
 20
21 /**
 22  * @author Dale "Ducky" Lotts
 23  * @author Siegfried Puchbauer
 24  * @since 2012/08/13
 25  */
 26
27 public class DomainClassMarshaller implements ObjectMarshaller {
 28
29  private GrailsApplication application;
 30
31  private boolean includeVersion = false;
 32  private ProxyHandler proxyHandler;
 33
34  public DomainClassMarshaller(boolean includeVersion, GrailsApplication application) {
 35  this(includeVersion, new DefaultProxyHandler(), application);
 36  }
 37
38  public DomainClassMarshaller(boolean includeVersion, ProxyHandler proxyHandler, GrailsApplication application) {
 39  this.includeVersion = includeVersion;
 40  this.proxyHandler = proxyHandler;
 41  this.application = application;
 42  }
 43
44  protected void asShortObject(Object refObj, JSON json, GrailsDomainClassProperty idProperty, GrailsDomainClass referencedDomainClass) throws ConverterException {
 45  Object idValue;
 46
47  if (proxyHandler instanceof EntityProxyHandler) {
 48  idValue = ((EntityProxyHandler) proxyHandler).getProxyIdentifier(refObj);
 49  if (idValue == null) {
 50  idValue = extractValue(refObj, idProperty);
 51  }
 52  }
 53  else {
 54  idValue = extractValue(refObj, idProperty);
 55  }
 56  JSONWriter writer = json.getWriter();
 57  writer.object();
 58  final clazz = referencedDomainClass.getClazz()
 59
60  if (!getIgnoredProperties(clazz).contains("class")) {
 61  writer.key("class").value(referencedDomainClass.getName());
 62  }
 63
64  writer.key("id").value(idValue);
 65
66  GrailsDomainClassProperty[] properties = referencedDomainClass.getPersistentProperties();
 67
68  Set includeProperties = getIncludedProperties(clazz)
 69
70  BeanWrapper beanWrapper = new BeanWrapperImpl(refObj);
 71
72  for (GrailsDomainClassProperty property : properties) {
 73  if (includeProperties.contains(property.name)) {
 74  writeProperty(property, beanWrapper, json)
 75  }
 76  }
 77
78  writer.endObject();
 79  }
 80
81  protected Object extractValue(Object domainObject, GrailsDomainClassProperty property) {
 82  BeanWrapper beanWrapper = new BeanWrapperImpl(domainObject);
 83  return beanWrapper.getPropertyValue(property.getName());
 84  }
 85
86  private Set getIgnoredProperties(Class target) {
 87  Set ignoredProperties = new HashSet<>();
 88
89  if (target.isAnnotationPresent(JsonIgnoreProperties)) {
 90  JsonIgnoreProperties annotation = target.getAnnotation(JsonIgnoreProperties)
 91  ignoredProperties.addAll(Arrays.asList(annotation.value()))
 92  }
 93
94  final ignored = target.declaredFields.findAll { field -> field.isAnnotationPresent(JsonIgnore)}
 95  for (Field field : ignored) {
 96  ignoredProperties.add(field.name)
 97  }
 98  ignoredProperties
 99  }
100
101  private Set getIncludedProperties(Class target) {
102  Set includeProperties = new HashSet<>();
103  final includes = target.declaredFields.findAll { field -> field.isAnnotationPresent(JsonInclude)}
104
105  for (Field field : includes) {
106  includeProperties.add(field.name)
107  }
108  includeProperties
109  }
110
111  public boolean isIncludeVersion() {
112  return includeVersion;
113  }
114
115  protected boolean isRenderDomainClassRelations() {
116  return false;
117  }
118
119
120  @SuppressWarnings([ "unchecked", "rawtypes" ])
121
122  public void marshalObject(Object value, JSON json) throws ConverterException {
123  JSONWriter writer = json.getWriter();
124  value = proxyHandler.unwrapIfProxy(value);
125  Class clazz = value.getClass();
126
127  GrailsDomainClass domainClass = (GrailsDomainClass)application.getArtefact(
128  DomainClassArtefactHandler.TYPE, ConverterUtil.trimProxySuffix(clazz.getName()));
129  BeanWrapper beanWrapper = new BeanWrapperImpl(value);
130
131  writer.object();
132
133  GrailsDomainClassProperty[] properties = domainClass.getPersistentProperties();
134
135  Set ignoredProperties = getIgnoredProperties(value.getClass())
136
137  if (!ignoredProperties.contains("class")) {
138  writer.key("class").value(domainClass.getClazz().getName())
139  };
140
141  GrailsDomainClassProperty id = domainClass.getIdentifier();
142  Object idValue = extractValue(value, id);
143
144  json.property("id", idValue);
145
146  if (isIncludeVersion()) {
147  GrailsDomainClassProperty versionProperty = domainClass.getVersion();
148  Object version = extractValue(value, versionProperty);
149  json.property("version", version);
150  }
151
152  for (GrailsDomainClassProperty property : properties) {
153  if (!ignoredProperties.contains(property.name)) {
154  writeProperty(property, beanWrapper, json)
155  }
156  }
157  writer.endObject();
158  }
159
160  public boolean supports(Object object) {
161  String name = ConverterUtil.trimProxySuffix(object.getClass().getName());
162  return application.isArtefactOfType(DomainClassArtefactHandler.TYPE, name);
163  }
164
165  private void writeProperty(GrailsDomainClassProperty property, BeanWrapperImpl beanWrapper, JSON json) {
166  json.getWriter().key(property.getName());
167  if (!property.isAssociation()) {
168  // Write non-relation property
169  Object val = beanWrapper.getPropertyValue(property.getName());
170  json.convertAnother(val);
171  }
172  else {
173  Object referenceObject = beanWrapper.getPropertyValue(property.getName());
174  if (isRenderDomainClassRelations()) {
175  if (referenceObject == null) {
176  json.getWriter().value(null);
177  }
178  else {
179  referenceObject = proxyHandler.unwrapIfProxy(referenceObject);
180  if (referenceObject instanceof SortedMap) {
181  referenceObject = new TreeMap((SortedMap) referenceObject);
182  }
183  else if (referenceObject instanceof SortedSet) {
184  referenceObject = new TreeSet((SortedSet) referenceObject);
185  }
186  else if (referenceObject instanceof Set) {
187  referenceObject = new HashSet((Set) referenceObject);
188  }
189  else if (referenceObject instanceof Map) {
190  referenceObject = new HashMap((Map) referenceObject);
191  }
192  else if (referenceObject instanceof Collection) {
193  referenceObject = new ArrayList((Collection) referenceObject);
194  }
195  json.convertAnother(referenceObject);
196  }
197  }
198  else {
199  if (referenceObject == null) {
200  json.value(null);
201  }
202  else {
203  GrailsDomainClass referencedDomainClass = property.getReferencedDomainClass();
204
205  // Embedded are now always fully rendered
206  if (referencedDomainClass == null || property.isEmbedded() || GrailsClassUtils.isJdk5Enum(property.getType())) {
207  json.convertAnother(referenceObject);
208  }
209  else if (property.isOneToOne() || property.isManyToOne() || property.isEmbedded()) {
210  asShortObject(referenceObject, json, referencedDomainClass.getIdentifier(), referencedDomainClass);
211  }
212  else {
213  GrailsDomainClassProperty referencedIdProperty = referencedDomainClass.getIdentifier();
214  @SuppressWarnings("unused")
215  String refPropertyName = referencedDomainClass.getPropertyName();
216  if (referenceObject instanceof Collection) {
217  Collection o = (Collection) referenceObject;
218  json.getWriter().array();
219  for (Object el : o) {
220  asShortObject(el, json, referencedIdProperty, referencedDomainClass);
221  }
222  json.getWriter().endArray();
223  }
224  else if (referenceObject instanceof Map) {
225  Map map = (Map) referenceObject;
226  for (Map.Entry entry : map.entrySet()) {
227  String key = String.valueOf(entry.getKey());
228  Object o = entry.getValue();
229  json.getWriter().object();
230  json.getWriter().key(key);
231  asShortObject(o, json, referencedIdProperty, referencedDomainClass);
232  json.getWriter().endObject();
233  }
234  }
235  }
236  }
237  }
238  }
239  }
240 }