Details
-
Type:
Improvement
-
Status:
Closed
-
Priority:
Minor
-
Resolution: Fixed
-
Affects Version/s: None
-
Fix Version/s: 2.0 final
-
Component/s: Converters
-
Labels:None
-
Environment:OSX 10.5.8
-
Testcase included:yes
Description
I was attempting to write some unit tests (NOT integration tests) for some code that makes use of the Grails JSON converters, and I encountered some frustrating behavior. Code that worked fine within the context of the Grails application threw exceptions when run in my tests, and it took several hours of digging through google and the Grails source code to figure out why: the JSON converter relies on configuration code that occurs when Grails starts up, and has no defaults to fall back upon when running outside the application.
After acquiring more knowledge about how the converters work, I see why this might be: you can write custom marshallers to provide class-specific custom JSON encoding logic that can be seamlessly integrated throughout your application. It makes sense that this is accomplished by using a Singleton Configuration object that the converter uses behind the scenes to figure out how to convert things. However, this is extremely unintuitive (not to mention frustrating) for someone who has no need for such customization nor any expectation that something seemingly unrelated to the Grails application (such as converting a POGO to JSON) would require an integration test in order to function properly.
I propose that, in cases where the Grails application does not exist, if the JSON converter (or other converters, for that matter) is invoked, default ObjectMarshallers should be added automatically to its configuration object so that it provides the expected behavior. Obviously, if custom ObjectMarshallers are used or other application specific JSON configuration takes place, an integration test would be required- but it's silly not to have the default marshallers available outside the application.
Here's an example of a unit test that illustrates the point:
public class JsonConverterTests extends GroovyTestCase{ def data def expectedJson void setUp(){ def myHomeAddress = [ building: "123", street: "Main Street", city: "Anytown", country: "USA", zip: 12345] def myWorkAddress = [ building: "567", street: "Workplace Ave", city: "Anycity", country: "USA", zip: 54321] def john = [ name: "John Doe", address: [myHomeAddress, myWorkAddress]] data = [people: [john]] expectedJson = '{"people":[{"name":"John Doe","address":[{"building":"123","street":"Main Street","city":"Anytown","country":"USA","zip":12345},{"building":"567","street":"Workplace Ave","city":"Anycity","country":"USA","zip":54321}]}]}' } void testConverter(){ String json = "" shouldFail org.apache.commons.lang.UnhandledException, { // UnhandledException triggered when converter throws a json = data as JSON // org.codehaus.groovy.grails.web.converters.exceptions.ConverterException } initMarshallers() json = data as JSON assertEquals expectedJson, json } private void initMarshallers(){ List<ObjectMarshaller<JSON>> marshallers = new ArrayList<ObjectMarshaller<JSON>>(); marshallers.add(new org.codehaus.groovy.grails.web.converters.marshaller.json.ArrayMarshaller()); marshallers.add(new org.codehaus.groovy.grails.web.converters.marshaller.json.ByteArrayMarshaller()); marshallers.add(new org.codehaus.groovy.grails.web.converters.marshaller.json.CollectionMarshaller()); marshallers.add(new org.codehaus.groovy.grails.web.converters.marshaller.json.MapMarshaller()); marshallers.add(new org.codehaus.groovy.grails.web.converters.marshaller.json.EnumMarshaller()); marshallers.add(new org.codehaus.groovy.grails.web.converters.marshaller.ProxyUnwrappingMarshaller<JSON>()); marshallers.add(new org.codehaus.groovy.grails.web.converters.marshaller.json.DateMarshaller()); marshallers.add(new org.codehaus.groovy.grails.web.converters.marshaller.json.ToStringBeanMarshaller()); boolean includeDomainVersion = true; marshallers.add(new org.codehaus.groovy.grails.web.converters.marshaller.json.DomainClassMarshaller(includeDomainVersion)); marshallers.add(new org.codehaus.groovy.grails.web.converters.marshaller.json.GroovyBeanMarshaller()); marshallers.add(new org.codehaus.groovy.grails.web.converters.marshaller.json.GenericJavaBeanMarshaller()); DefaultConverterConfiguration<JSON> cfg = new DefaultConverterConfiguration<JSON>(marshallers); cfg.setEncoding("UTF-8"); cfg.setCircularReferenceBehaviour(Converter.CircularReferenceBehaviour.DEFAULT) cfg.setPrettyPrint(false); ConvertersConfigurationHolder.setDefaultConfiguration(JSON.class, new ChainedConverterConfiguration<JSON>(cfg)); } }
The initMarshallers() method just creates a default configuration for the JSON converter that is the same as provided in org.codehaus.groovy.grails.web.converters.configuration.ConvertersConfigurationInitializer.initJSONConfiguration(), save that all of the Grails config references have been replaced with their default values. If something similar could be used by default if no configuration object has been manually initialized, I think that the converter functionality would be much improved.
As a workaround, it's trivial to extend the Grails JSON object in a way that sets up the default ObjectMarshallers if they don't exist already:
Just import util.converters.JSON instead of grails.converters.JSON, and everything else works seamlessly.