Grails
  1. Grails
  2. GRAILS-6222

Default JSON ObjectMarshallers should be configured when Grails ApplicationContext does not exist

    Details

    • Type: Improvement Improvement
    • Status: Closed
    • Priority: Minor 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:

      JsonConverterTests.groovy
      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.

        Activity

        Hide
        Ellery Crane added a comment -

        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:

        JSON.groovy
        package util.converters
        
        import org.codehaus.groovy.grails.web.converters.configuration.ConvertersConfigurationHolder
        import org.codehaus.groovy.grails.web.converters.configuration.ConverterConfiguration
        import org.codehaus.groovy.grails.web.converters.configuration.DefaultConverterConfiguration
        import org.codehaus.groovy.grails.web.converters.marshaller.ObjectMarshaller
        import org.codehaus.groovy.grails.web.converters.Converter
        import org.codehaus.groovy.grails.web.converters.configuration.ChainedConverterConfiguration
        
        class JSON extends grails.converters.JSON{
        
          public JSON(Object target) {
            super(target)
          }
        
          @Override
          protected ConverterConfiguration<grails.converters.JSON> initConfig() {    
            ConverterConfiguration config = super.initConfig()
            if(config.getOrderedObjectMarshallers().size() == 0){      
              initDefaultMarshallers()
              config = super.initConfig()
            }
            return config
          }
        
          private void initDefaultMarshallers(){
            List<ObjectMarshaller<grails.converters.JSON>> marshallers = new ArrayList<ObjectMarshaller<grails.converters.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<grails.converters.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<grails.converters.JSON> cfg = new DefaultConverterConfiguration<grails.converters.JSON>(marshallers);
            cfg.setEncoding("UTF-8");
            cfg.setCircularReferenceBehaviour(Converter.CircularReferenceBehaviour.DEFAULT)
            cfg.setPrettyPrint(false);
            ConvertersConfigurationHolder.setDefaultConfiguration(grails.converters.JSON.class, new ChainedConverterConfiguration<grails.converters.JSON>(cfg));
          }
        }
        

        Just import util.converters.JSON instead of grails.converters.JSON, and everything else works seamlessly.

        Show
        Ellery Crane added a comment - 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: JSON.groovy package util.converters import org.codehaus.groovy.grails.web.converters.configuration.ConvertersConfigurationHolder import org.codehaus.groovy.grails.web.converters.configuration.ConverterConfiguration import org.codehaus.groovy.grails.web.converters.configuration.DefaultConverterConfiguration import org.codehaus.groovy.grails.web.converters.marshaller.ObjectMarshaller import org.codehaus.groovy.grails.web.converters.Converter import org.codehaus.groovy.grails.web.converters.configuration.ChainedConverterConfiguration class JSON extends grails.converters.JSON{ public JSON( Object target) { super (target) } @Override protected ConverterConfiguration<grails.converters.JSON> initConfig() { ConverterConfiguration config = super .initConfig() if (config.getOrderedObjectMarshallers().size() == 0){ initDefaultMarshallers() config = super .initConfig() } return config } private void initDefaultMarshallers(){ List<ObjectMarshaller<grails.converters.JSON>> marshallers = new ArrayList<ObjectMarshaller<grails.converters.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<grails.converters.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<grails.converters.JSON> cfg = new DefaultConverterConfiguration<grails.converters.JSON>(marshallers); cfg.setEncoding( "UTF-8" ); cfg.setCircularReferenceBehaviour(Converter.CircularReferenceBehaviour.DEFAULT) cfg.setPrettyPrint( false ); ConvertersConfigurationHolder.setDefaultConfiguration(grails.converters.JSON.class, new ChainedConverterConfiguration<grails.converters.JSON>(cfg)); } } Just import util.converters.JSON instead of grails.converters.JSON, and everything else works seamlessly.
        Hide
        Andrew Gilbert added a comment -

        To save anyone else time, under 2.x just include ControllerUnitTextMixin in the mix and it will do all this nonsense for you, ie:

        @TestMixin([GrailsUnitTestMixin, ControllerUnitTestMixin])
        class FooBarTests {
        }

        Show
        Andrew Gilbert added a comment - To save anyone else time, under 2.x just include ControllerUnitTextMixin in the mix and it will do all this nonsense for you, ie: @TestMixin( [GrailsUnitTestMixin, ControllerUnitTestMixin] ) class FooBarTests { }
        Hide
        Graeme Rocher added a comment -

        marking as fixed as as mentioned in the previous comment the mixins can handle this for you

        Show
        Graeme Rocher added a comment - marking as fixed as as mentioned in the previous comment the mixins can handle this for you

          People

          • Assignee:
            Graeme Rocher
            Reporter:
            Ellery Crane
          • Votes:
            8 Vote for this issue
            Watchers:
            5 Start watching this issue

            Dates

            • Created:
              Updated:
              Resolved:
              Last Reviewed:

              Development