Menu

Map<String,Object> deserialization...

Help
2012-08-24
2013-03-19
  • Philip Mair

    Philip Mair - 2012-08-24

    I'm trying to serialize and deserialize a Entity with a Map<String,Object> member.

    The serialization is working fine, but the deserialization fails with java.lang.ClassCastException: java.lang.String cannot be cast to java.util.Map

    I'm using flexjson-2.1

    DemoClass:

    import java.util.HashMap;
    import java.util.Map;
    import flexjson.JSONDeserializer;
    import flexjson.JSONSerializer;
    public class MapDemo {
        /**
         * @param args
         */
        public static void main(String[] args) {
           
            MapDemo demo = new MapDemo();
            demo.getData().put("key1", "value1");
            demo.getData().put("key2", "value2");
            
            JSONSerializer jsonSerializer = new JSONSerializer();
            
            String json = jsonSerializer.deepSerialize( demo );
            
            System.out.println( json );
            
            JSONDeserializer<MapDemo> jsonDeserializer = new JSONDeserializer<MapDemo>();
            
            MapDemo result = jsonDeserializer.deserialize( json );
            
            System.out.println( "key1 = " + result.getData().get( "key1" ) );
            System.out.println( "key2 = " + result.getData().get( "key2" ) );
        }
        
        private Map<String, Object> data = new HashMap<String, Object>();
        public Map<String, Object> getData() {
            return data;
        }
        public void setData(Map<String, Object> data) {
            this.data = data;
        }
        
    }
    

    sysout:

    {"class":"MapDemo","data":{"key2":"value2","key1":"value1"}}
    Exception in thread "main" java.lang.ClassCastException: java.lang.String cannot be cast to java.util.Map
        at flexjson.factories.BeanObjectFactory.instantiate(BeanObjectFactory.java:17)
        at flexjson.ObjectBinder.bind(ObjectBinder.java:86)
        at flexjson.ObjectBinder.bindIntoMap(ObjectBinder.java:117)
        at flexjson.factories.MapObjectFactory.instantiate(MapObjectFactory.java:16)
        at flexjson.ObjectBinder.bind(ObjectBinder.java:86)
        at flexjson.ObjectBinder.bindIntoObject(ObjectBinder.java:139)
        at flexjson.factories.BeanObjectFactory.instantiate(BeanObjectFactory.java:17)
        at flexjson.ObjectBinder.bind(ObjectBinder.java:86)
        at flexjson.ObjectBinder.bind(ObjectBinder.java:65)
        at flexjson.JSONDeserializer.deserialize(JSONDeserializer.java:158)
        at MapDemo.main(MapDemo.java:28)
    

    Any suggestions ?

     
  • Philip Mair

    Philip Mair - 2012-08-24

    I fixed my Problem:

    I patched the BeanObjectFactory:

    package flexjson.factories;
    import flexjson.ObjectFactory;
    import flexjson.ObjectBinder;
    import flexjson.JSONException;
    import java.lang.reflect.Type;
    import java.lang.reflect.Constructor;
    import java.lang.reflect.InvocationTargetException;
    import java.util.Map;
    public class BeanObjectFactory implements ObjectFactory {
        public Object instantiate(ObjectBinder context, Object value, Type targetType, Class targetClass) {
            try {
                Object target = instantiate( targetClass );
                if( value instanceof Map )
                    return context.bindIntoObject( (Map) value, target, targetType );
                else
                    return value;
            } catch (InstantiationException e) {
                throw new JSONException(context.getCurrentPath() + ":There was an exception trying to instantiate an instance of " + targetClass.getName(), e );
            } catch (IllegalAccessException e) {
                throw new JSONException(context.getCurrentPath() + ":There was an exception trying to instantiate an instance of " + targetClass.getName(), e );
            } catch (InvocationTargetException e) {
                throw new JSONException(context.getCurrentPath() + ":There was an exception trying to instantiate an instance of " + targetClass.getName(), e );
            } catch (NoSuchMethodException e) {
                throw new JSONException(context.getCurrentPath() + ": " + targetClass.getName() + " lacks a no argument constructor.  Flexjson will instantiate any protected, private, or public no-arg constructor.", e );
            }
        }
        protected Object instantiate( Class clazz ) throws IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException {
            Constructor constructor = clazz.getDeclaredConstructor();
            constructor.setAccessible( true );
            return constructor.newInstance();
        }
    }
    
     
  • allenru

    allenru - 2012-11-08

    I think there's a better way to fix this issue.  The biggest clue is that, with your fix, the code is still calling the BeanObjectFactory to create a primitive value.  It should be using the StringObjectFactory.  After all, that's the type that needs to be created.  So, where did the code mistakenly choose to type the string value as a bean?

    Take a look at MapObjectFactory…

    public class MapObjectFactory implements ObjectFactory {
        public Object instantiate(ObjectBinder context, Object value, Type targetType, Class targetClass) {
            if( targetType != null ) {
                if( targetType instanceof ParameterizedType ) {
                    ParameterizedType ptype = (ParameterizedType) targetType;
                    return context.bindIntoMap( (Map)value, new HashMap<Object,Object>(), ptype.getActualTypeArguments()[0], ptype.getActualTypeArguments()[1] );
                }
            }
            return context.bindIntoMap( (Map)value, new HashMap<Object,Object>(), null, null );
        }
    }
    

    Note how it assumes that if the map is a parameterized type, then the generic type is the true type of the key and value.  In your use case (and mine) the generic type of the value is Object but the actual type is a primitive.  I think it's this assumption that's in error.

    I rewrote the MapObjectFactory as follows:

        public class MapObjectFactory implements ObjectFactory {
            public Object instantiate(ObjectBinder context, Object value, Type targetType, Class targetClass) {
                if( targetType != null ) {
                    if( targetType instanceof ParameterizedType ) {
                        ParameterizedType ptype = (ParameterizedType) targetType;
                        return context.bindIntoMap( (Map)value, new HashMap<Object,Object>(), 
                                ptype.getActualTypeArguments()[0]==Object.class?null:ptype.getActualTypeArguments()[0], 
                                ptype.getActualTypeArguments()[1]==Object.class?null:ptype.getActualTypeArguments()[1]);
                    }
                }
                return context.bindIntoMap( (Map)value, new HashMap<Object,Object>(), null, null );
            }
        }
    

    This doesn't address all cases of sub-classing, but it proves the point with regard to Object as the generic type.  By forcing the type to null, I delay the determination of the type to later.  It is the same as if the type was not specified by generics.  Most importantly, it works.

    I'd love to get the authors input on this.  Particularly on the subject of the generic type being more specific than object but still a super-class of the actual value type.

    Thanks!
    -Russell

     
  • Charlie Hubbard

    Charlie Hubbard - 2012-11-08

    Ok I took Allenru's fix and tested it against our existing unit tests, added a few more, and marked the defect fix.  It's checked in.  Thanks for everyone for your help and fixes.

     
  • Bruno Braga

    Bruno Braga - 2012-12-26

    Is it fixed in witch version?
    I have the same problem.

     

Log in to post a comment.