You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@brooklyn.apache.org by he...@apache.org on 2021/08/14 01:54:54 UTC

[brooklyn-server] 02/06: serialization and deserialization that puts/gets objects to a backing map and writes references

This is an automated email from the ASF dual-hosted git repository.

heneveld pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/brooklyn-server.git

commit bc2583d402b8ac1e9cde1350904e6a73b0646e23
Author: Alex Heneveld <al...@cloudsoftcorp.com>
AuthorDate: Sat Aug 14 01:50:35 2021 +0100

    serialization and deserialization that puts/gets objects to a backing map and writes references
---
 core/pom.xml                                       |   4 +
 .../core/resolve/jackson/BeanWithTypeUtils.java    |  19 ++-
 ...BrooklynRegisteredTypeJacksonSerialization.java |  10 ++
 .../jackson/ObjectReferencingSerialization.java    | 172 +++++++++++++++++++++
 .../jackson/WrappedValuesSerialization.java        |  11 +-
 .../brooklyn/util/core/flags/TypeCoercions.java    |   8 +-
 .../BrooklynMiscJacksonSerializationTest.java      | 165 +++++++++++++++++++-
 .../core/resolve/jackson/MapperTestFixture.java    |  18 +++
 .../util/core/internal/TypeCoercionsTest.java      |  15 ++
 9 files changed, 414 insertions(+), 8 deletions(-)

diff --git a/core/pom.xml b/core/pom.xml
index cd5a2c5..c03c64f 100644
--- a/core/pom.xml
+++ b/core/pom.xml
@@ -164,6 +164,10 @@
             <artifactId>jackson-databind</artifactId>
         </dependency>
         <dependency>
+            <groupId>com.fasterxml.jackson.dataformat</groupId>
+            <artifactId>jackson-dataformat-yaml</artifactId>
+        </dependency>
+        <dependency>
             <groupId>com.fasterxml.jackson.jaxrs</groupId>
             <artifactId>jackson-jaxrs-json-provider</artifactId>
         </dependency>
diff --git a/core/src/main/java/org/apache/brooklyn/core/resolve/jackson/BeanWithTypeUtils.java b/core/src/main/java/org/apache/brooklyn/core/resolve/jackson/BeanWithTypeUtils.java
index 7c31bf9..cf30094 100644
--- a/core/src/main/java/org/apache/brooklyn/core/resolve/jackson/BeanWithTypeUtils.java
+++ b/core/src/main/java/org/apache/brooklyn/core/resolve/jackson/BeanWithTypeUtils.java
@@ -18,9 +18,15 @@
  */
 package org.apache.brooklyn.core.resolve.jackson;
 
+import com.fasterxml.jackson.annotation.JsonAutoDetect;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.PropertyAccessor;
 import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.DeserializationFeature;
 import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
 import com.fasterxml.jackson.databind.json.JsonMapper;
+import com.fasterxml.jackson.dataformat.yaml.YAMLMapper;
 import com.google.common.reflect.TypeToken;
 import java.util.*;
 import java.util.Map.Entry;
@@ -50,8 +56,14 @@ public class BeanWithTypeUtils {
 
     /** also see {@link org.apache.brooklyn.util.core.json.BrooklynObjectsJsonMapper#newMapper(ManagementContext)} */
     public static ObjectMapper newMapper(ManagementContext mgmt, boolean allowRegisteredTypes, BrooklynClassLoadingContext loader, boolean allowBasicJavaTypes) {
-        JsonMapper mapper = newSimpleMapper();
+        return applyCommonMapperConfig(newSimpleMapper(), mgmt, allowRegisteredTypes, loader, allowBasicJavaTypes);
+    }
+
+    public static ObjectMapper newYamlMapper(ManagementContext mgmt, boolean allowRegisteredTypes, BrooklynClassLoadingContext loader, boolean allowBasicJavaTypes) {
+        return applyCommonMapperConfig(newSimpleYamlMapper(), mgmt, allowRegisteredTypes, loader, allowBasicJavaTypes);
+    }
 
+    public static ObjectMapper applyCommonMapperConfig(ObjectMapper mapper, ManagementContext mgmt, boolean allowRegisteredTypes, BrooklynClassLoadingContext loader, boolean allowBasicJavaTypes) {
         BrooklynRegisteredTypeJacksonSerialization.apply(mapper, mgmt, allowRegisteredTypes, loader, allowBasicJavaTypes);
         WrappedValuesSerialization.apply(mapper, mgmt);
         mapper = new ConfigurableBeanDeserializerModifier()
@@ -68,6 +80,11 @@ public class BeanWithTypeUtils {
         return JsonMapper.builder().build();
     }
 
+    public static YAMLMapper newSimpleYamlMapper() {
+        // for use with json maps (no special type resolution, even the field "type" is ignored)
+        return YAMLMapper.builder().build();
+    }
+
     public static boolean isPureJson(Object o) {
         return isJsonAndOthers(o, oo -> false);
     }
diff --git a/core/src/main/java/org/apache/brooklyn/core/resolve/jackson/BrooklynRegisteredTypeJacksonSerialization.java b/core/src/main/java/org/apache/brooklyn/core/resolve/jackson/BrooklynRegisteredTypeJacksonSerialization.java
index e9724c8..b7dbad4 100644
--- a/core/src/main/java/org/apache/brooklyn/core/resolve/jackson/BrooklynRegisteredTypeJacksonSerialization.java
+++ b/core/src/main/java/org/apache/brooklyn/core/resolve/jackson/BrooklynRegisteredTypeJacksonSerialization.java
@@ -18,9 +18,12 @@
  */
 package org.apache.brooklyn.core.resolve.jackson;
 
+import com.fasterxml.jackson.annotation.JsonAutoDetect;
+import com.fasterxml.jackson.annotation.JsonInclude;
 import com.fasterxml.jackson.annotation.JsonInclude.Include;
 import com.fasterxml.jackson.annotation.JsonTypeInfo;
 import com.fasterxml.jackson.annotation.JsonTypeInfo.As;
+import com.fasterxml.jackson.annotation.PropertyAccessor;
 import com.fasterxml.jackson.core.JsonParser;
 import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.databind.*;
@@ -122,6 +125,11 @@ public class BrooklynRegisteredTypeJacksonSerialization {
                     return context.constructType(fromLoader.get());
                 }
             }
+            // TODO - this would be nice to support complex types
+//            if (type is present in a registered type) {
+//                get the bundle of registered type
+//                use that classloader to instantiate the type
+//            }
             if (allowPojoJavaTypes) {
                 return super.typeFromId(context, id);
             }
@@ -220,6 +228,8 @@ public class BrooklynRegisteredTypeJacksonSerialization {
     public static ObjectMapper apply(ObjectMapper mapper, ManagementContext mgmt, boolean allowRegisteredTypes, BrooklynClassLoadingContext loader, boolean allowPojoJavaTypes) {
         // the type resolver is extended to recognise brooklyn registered type names
         // and return a subtype of jackson JavaType
+        mapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS);
+
         mapper.setDefaultTyping(new BrtTypeResolverBuilder(mgmt, allowRegisteredTypes, loader, allowPojoJavaTypes));
 
         SimpleModule module = new SimpleModule();
diff --git a/core/src/main/java/org/apache/brooklyn/core/resolve/jackson/ObjectReferencingSerialization.java b/core/src/main/java/org/apache/brooklyn/core/resolve/jackson/ObjectReferencingSerialization.java
new file mode 100644
index 0000000..27005c1
--- /dev/null
+++ b/core/src/main/java/org/apache/brooklyn/core/resolve/jackson/ObjectReferencingSerialization.java
@@ -0,0 +1,172 @@
+package org.apache.brooklyn.core.resolve.jackson;
+
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.core.JsonToken;
+import com.fasterxml.jackson.databind.BeanDescription;
+import com.fasterxml.jackson.databind.DeserializationContext;
+import com.fasterxml.jackson.databind.JavaType;
+import com.fasterxml.jackson.databind.JsonDeserializer;
+import com.fasterxml.jackson.databind.JsonMappingException;
+import com.fasterxml.jackson.databind.JsonSerializer;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializerProvider;
+import com.fasterxml.jackson.databind.cfg.SerializerFactoryConfig;
+import com.fasterxml.jackson.databind.jsontype.TypeSerializer;
+import com.fasterxml.jackson.databind.ser.BeanSerializerFactory;
+import com.fasterxml.jackson.databind.ser.SerializerFactory;
+import com.fasterxml.jackson.databind.ser.std.StdSerializer;
+import com.google.common.collect.BiMap;
+import com.google.common.collect.HashBiMap;
+import java.io.IOException;
+import java.util.Map;
+import org.apache.brooklyn.core.resolve.jackson.BrooklynJacksonSerializationUtils.ConfigurableBeanDeserializerModifier;
+import org.apache.brooklyn.util.text.Identifiers;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class ObjectReferencingSerialization {
+
+    private static final Logger LOG = LoggerFactory.getLogger(ObjectReferencingSerialization.class);
+
+    public ObjectMapper useMapper(ObjectMapper mapper) {
+        BiMap<String,Object> backingMap = HashBiMap.create();
+        mapper.setSerializerFactory(ObjectReferencingSerializerFactory.extending(mapper.getSerializerFactory(), new ObjectReferenceSerializer(backingMap)));
+        mapper = new ConfigurableBeanDeserializerModifier()
+                .addDeserializerWrapper(
+                        d -> new ObjectReferencingJsonDeserializer(d, backingMap)
+                ).apply(mapper);
+
+//        mapper.registerModule(new SimpleModule()
+//                .addSerializer(Object.class, new ObjectReferenceSerializer(backingMap))
+//                .addDeserializer(Object.class, new ObjectReferenceDeserializer(backingMap))
+//        );
+        return mapper;
+    }
+
+
+    static class ObjectReference {
+        String id;
+        public ObjectReference() {}
+        public ObjectReference(String id) { this.id = id; }
+    }
+
+
+    static class ObjectReferenceSerializer extends StdSerializer<Object> {
+        private final BiMap<String, Object> backingMap;
+
+        public ObjectReferenceSerializer(BiMap<String, Object> backingMap) {
+            super(Object.class);
+            this.backingMap = backingMap;
+        }
+
+        @Override
+        public void serializeWithType(Object value, JsonGenerator gen, SerializerProvider serializers, TypeSerializer typeSer) throws IOException {
+            serialize(value, gen, serializers);
+        }
+
+        @Override
+        public void serialize(Object value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
+            String id = backingMap.inverse().get(value);
+            if (id==null) {
+                id = Identifiers.makeRandomId(12);
+                backingMap.put(id, value);
+            }
+
+            gen.writeObjectRef(id);
+
+//            serializers.findValueSerializer(Map.class, null).serializeWithType(MutableMap.of("@ref", id), gen, serializers,
+//                    serializers.findTypeSerializer(serializers.constructType(Object.class)));
+        }
+    }
+
+    static class ObjectReferenceDeserializer extends JsonDeserializer<Object> {
+        public ObjectReferenceDeserializer(Map<String, Object> backingMap) {
+        }
+
+        @Override
+        public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
+            return null;
+        }
+    }
+
+    public static class ObjectReferencingSerializerFactory extends BeanSerializerFactory {
+        private final ObjectReferenceSerializer serializer;
+
+        protected ObjectReferencingSerializerFactory(ObjectReferenceSerializer serializer, SerializerFactoryConfig config) {
+            super(config);
+            this.serializer = serializer;
+        }
+
+        public static ObjectReferencingSerializerFactory extending(SerializerFactory factory, ObjectReferenceSerializer serializer) {
+            if (factory == null) return new ObjectReferencingSerializerFactory(serializer, null);
+            if (factory instanceof BeanSerializerFactory) return new ObjectReferencingSerializerFactory(serializer, ((BeanSerializerFactory) factory).getFactoryConfig() );
+            throw new IllegalStateException("Cannot extend "+factory);
+        }
+        @Override
+        public ObjectReferencingSerializerFactory withConfig(SerializerFactoryConfig config) {
+            if (_factoryConfig == config) return this;
+            return new ObjectReferencingSerializerFactory(serializer, config);
+        }
+
+        // --- our special behaviour
+
+        @Override
+        protected JsonSerializer<Object> constructBeanOrAddOnSerializer(SerializerProvider prov, JavaType type, BeanDescription beanDesc, boolean staticTyping) throws JsonMappingException {
+            return serializer;
+        }
+    }
+
+    static class ObjectReferencingJsonDeserializer extends JacksonBetterDelegatingDeserializer {
+        private final BiMap<String, Object> backingMap;
+
+        public ObjectReferencingJsonDeserializer(JsonDeserializer<?> d, BiMap<String, Object> backingMap) {
+            super(d, (d2) -> new ObjectReferencingJsonDeserializer(d2, backingMap));
+            this.backingMap = backingMap;
+        }
+
+        @Override
+        protected Object deserializeWrapper(JsonParser jp, DeserializationContext ctxt, BiFunctionThrowsIoException<JsonParser, DeserializationContext, Object> nestedDeserialize) throws IOException {
+            String v = jp.getCurrentToken()== JsonToken.VALUE_STRING ? jp.getValueAsString() : null;
+            if (v!=null) {
+                Object result = backingMap.get(v);
+                if (result!=null) return result;
+            }
+            return nestedDeserialize.apply(jp, ctxt);
+        }
+    }
+//
+//    public static class ObjectReferencingDeserializerFactory extends BeanDeserializerFactory {
+//        protected ObjectReferencingDeserializerFactory(DeserializerFactoryConfig config) {
+//            super(config);
+//        }
+//
+//        public static ObjectReferencingDeserializerFactory extending(DeserializerFactory factory) {
+//            if (factory == null) return new ObjectReferencingDeserializerFactory(null);
+//            if (factory instanceof ObjectReferencingDeserializerFactory) return (ObjectReferencingDeserializerFactory) factory;
+//            if (factory instanceof BeanDeserializerFactory) return new ObjectReferencingDeserializerFactory( ((BeanDeserializerFactory) factory).getFactoryConfig() );
+//            throw new IllegalStateException("Cannot extend "+factory);
+//        }
+//        @Override
+//        public ObjectReferencingDeserializerFactory withConfig(DeserializerFactoryConfig config) {
+//            if (_factoryConfig == config) return this;
+//            return new ObjectReferencingDeserializerFactory(config);
+//        }
+//
+//        // --- our special behaviour
+//
+//
+//        @Override
+//        protected BeanDeserializerBuilder constructBeanDeserializerBuilder(DeserializationContext ctxt, BeanDescription beanDesc) {
+//            return new BeanDeserializerBuilder(beanDesc, ctxt) {
+//                {
+//                    _objectIdReader = new ObjectIdReader() {
+//
+//                    };
+//                }
+//            };
+//        }
+//    }
+
+}
diff --git a/core/src/main/java/org/apache/brooklyn/core/resolve/jackson/WrappedValuesSerialization.java b/core/src/main/java/org/apache/brooklyn/core/resolve/jackson/WrappedValuesSerialization.java
index 49b42a8..d91ee70 100644
--- a/core/src/main/java/org/apache/brooklyn/core/resolve/jackson/WrappedValuesSerialization.java
+++ b/core/src/main/java/org/apache/brooklyn/core/resolve/jackson/WrappedValuesSerialization.java
@@ -199,16 +199,19 @@ public class WrappedValuesSerialization {
             if (factory.getClass() == BeanSerializerFactory.class) return new NullWrappedValueSuppressingBeanSerializerFactory( ((BeanSerializerFactory) factory).getFactoryConfig() );
             throw new IllegalStateException("Cannot extend "+factory);
         }
+        @Override
+        public SerializerFactory withConfig(SerializerFactoryConfig config) {
+            if (_factoryConfig == config) return this;
+            return new NullWrappedValueSuppressingBeanSerializerFactory(config);
+        }
+
+        // --- our special behaviour
 
         @Override
         protected PropertyBuilder constructPropertyBuilder(SerializationConfig config, BeanDescription beanDesc) {
             return new NullWrappedValueSuppressingPropertyBuilder(config, beanDesc);
         }
 
-        public SerializerFactory withConfig(SerializerFactoryConfig config) {
-            if (_factoryConfig == config) return this;
-            return new NullWrappedValueSuppressingBeanSerializerFactory(config);
-        }
     }
 
     public static <T> T ensureWrappedValuesInitialized(T x) {
diff --git a/core/src/main/java/org/apache/brooklyn/util/core/flags/TypeCoercions.java b/core/src/main/java/org/apache/brooklyn/util/core/flags/TypeCoercions.java
index b708869..f10e676 100644
--- a/core/src/main/java/org/apache/brooklyn/util/core/flags/TypeCoercions.java
+++ b/core/src/main/java/org/apache/brooklyn/util/core/flags/TypeCoercions.java
@@ -354,13 +354,19 @@ public class TypeCoercions {
         public void registerBeanWithTypeAdapter() {
             // if we want to do bean-with-type coercion ... probably nice to do if it doesn't already match
             registerAdapter("80-bean-with-type", new TryCoercer() {
+
                 @Override
                 public <T> Maybe<T> tryCoerce(Object input, TypeToken<T> type) {
                     if (!(input instanceof Map || input instanceof Collection || Boxing.isPrimitiveOrBoxedObject(input))) {
                         return null;
                     }
                     if (BeanWithTypeUtils.isConversionRecommended(Maybe.of(input), type)) {
-                        return BeanWithTypeUtils.tryConvertOrAbsentUsingContext(Maybe.of(input), type);
+                        try {
+                            Maybe<T> result = BeanWithTypeUtils.tryConvertOrAbsentUsingContext(Maybe.of(input), type);
+                            return result;
+                        } catch (Exception e) {
+                            return Maybe.absent(e);
+                        }
                     }
                     return null;
                 }
diff --git a/core/src/test/java/org/apache/brooklyn/core/resolve/jackson/BrooklynMiscJacksonSerializationTest.java b/core/src/test/java/org/apache/brooklyn/core/resolve/jackson/BrooklynMiscJacksonSerializationTest.java
index a2297c7..1a609b9 100644
--- a/core/src/test/java/org/apache/brooklyn/core/resolve/jackson/BrooklynMiscJacksonSerializationTest.java
+++ b/core/src/test/java/org/apache/brooklyn/core/resolve/jackson/BrooklynMiscJacksonSerializationTest.java
@@ -18,16 +18,42 @@
  */
 package org.apache.brooklyn.core.resolve.jackson;
 
+import com.fasterxml.jackson.annotation.JsonIdentityInfo;
+import com.fasterxml.jackson.annotation.ObjectIdGenerator;
+import com.fasterxml.jackson.annotation.ObjectIdGenerator.IdKey;
+import com.fasterxml.jackson.annotation.ObjectIdGenerators.StringIdGenerator;
+import com.fasterxml.jackson.annotation.ObjectIdResolver;
+import com.fasterxml.jackson.databind.DeserializationConfig;
+import com.fasterxml.jackson.databind.JsonDeserializer;
+import com.fasterxml.jackson.databind.JsonSerializer;
+import com.fasterxml.jackson.databind.KeyDeserializer;
 import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationConfig;
+import com.fasterxml.jackson.databind.cfg.HandlerInstantiator;
+import com.fasterxml.jackson.databind.cfg.MapperConfig;
+import com.fasterxml.jackson.databind.introspect.Annotated;
+import com.fasterxml.jackson.databind.json.JsonMapper;
+import com.fasterxml.jackson.databind.jsontype.TypeIdResolver;
+import com.fasterxml.jackson.databind.jsontype.TypeResolverBuilder;
+import com.fasterxml.jackson.dataformat.yaml.YAMLMapper;
+import com.google.common.reflect.TypeToken;
+import java.util.Map;
 import org.apache.brooklyn.test.Asserts;
+import org.apache.brooklyn.util.collections.MutableMap;
+import org.apache.brooklyn.util.javalang.JavaClassNames;
 import org.apache.brooklyn.util.time.Duration;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 import org.testng.annotations.Test;
 
 public class BrooklynMiscJacksonSerializationTest implements MapperTestFixture {
 
-    public ObjectMapper mapper() {
-        ObjectMapper mapper = BeanWithTypeUtils.newMapper(null, false, null, true);
+    private static final Logger LOG = LoggerFactory.getLogger(BrooklynMiscJacksonSerializationTest.class);
+
+    private ObjectMapper mapper;
 
+    public ObjectMapper mapper() {
+        if (mapper==null) mapper = BeanWithTypeUtils.newMapper(null, false, null, true);
         return mapper;
     }
 
@@ -46,5 +72,140 @@ public class BrooklynMiscJacksonSerializationTest implements MapperTestFixture {
         Asserts.assertEquals(deser("\"1m\"", Duration.class), Duration.minutes(1));
     }
 
+    static class ObjWithoutIdentityInfoAnnotation {
+        String foo;
+
+        @Override
+        public String toString() {
+            return "Obj{" +
+                    "foo='" + foo + '\'' +
+                    "}@"+ System.identityHashCode(this);
+        }
+    }
+
+    public static class AllBeansIdentityHandler extends HandlerInstantiator {
+        @Override
+        public JsonDeserializer<?> deserializerInstance(DeserializationConfig config, Annotated annotated, Class<?> deserClass) {
+            return null;
+        }
+        @Override
+        public KeyDeserializer keyDeserializerInstance(DeserializationConfig config, Annotated annotated, Class<?> keyDeserClass) {
+            return null;
+        }
+        @Override
+        public JsonSerializer<?> serializerInstance(SerializationConfig config, Annotated annotated, Class<?> serClass) {
+            return null;
+        }
+        @Override
+        public TypeResolverBuilder<?> typeResolverBuilderInstance(MapperConfig<?> config, Annotated annotated, Class<?> builderClass) {
+            return null;
+        }
+        @Override
+        public TypeIdResolver typeIdResolverInstance(MapperConfig<?> config, Annotated annotated, Class<?> resolverClass) {
+            return null;
+        }
+
+        @Override
+        public ObjectIdGenerator<?> objectIdGeneratorInstance(MapperConfig<?> config, Annotated annotated, Class<?> implClass) {
+            return new StringIdGenerator();
+        }
+
+        @Override
+        public ObjectIdResolver resolverIdGeneratorInstance(MapperConfig<?> config, Annotated annotated, Class<?> implClass) {
+            return new MapBasedInstanceResolver();
+        }
+    }
+
+    static class MapBasedInstanceResolver implements ObjectIdResolver {
+
+        Map<IdKey,Object> objectsById = MutableMap.of();
+
+        @Override
+        public void bindItem(IdKey id, Object pojo) {
+            objectsById.put(id, pojo);
+        }
+
+        @Override
+        public Object resolveId(IdKey id) {
+            Object result = objectsById.get(id);
+            if (result!=null) return result;
+            // seems to happen for YAMLMapper, it doesn't call bindItem
+            LOG.warn("No object recorded for ID "+id+"; returning null during deserialization");
+            return null;
+        }
+
+        @Override
+        public ObjectIdResolver newForDeserialization(Object context) {
+            return this;
+        }
+
+        @Override
+        public boolean canUseFor(ObjectIdResolver resolverType) {
+            return true;
+        }
+    }
+
+    @JsonIdentityInfo(property="@object_id", generator=StringIdGenerator.class, resolver= MapBasedInstanceResolver.class)
+    static class ObjWithIdentityInfoAnnotation extends ObjWithoutIdentityInfoAnnotation {}
+
+    @Test
+    public void testHowObjectIdAndReferences() throws Exception {
+        mapper =
+                BeanWithTypeUtils.applyCommonMapperConfig(
+                    JsonMapper.builder().build()
+
+                // YAML doesn't seem to call "bindItem" whereas JSON mapper does
+//                    YAMLMapper.builder().
+////                        configure(YAMLGenerator.Feature.USE_NATIVE_OBJECT_ID, true).
+//                        build()
+
+                    , null, false, null, true)
+        ;
+//        mapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS);
+
+        ObjWithIdentityInfoAnnotation f1 = new ObjWithIdentityInfoAnnotation(); f1.foo = "1";
+        ObjWithIdentityInfoAnnotation f2 = new ObjWithIdentityInfoAnnotation(); f2.foo = "2";
+        String out = ser(MutableMap.of("a", f1, "b", f2, "c", f1));
+        LOG.info("Result of "+ JavaClassNames.niceClassAndMethod()+": "+out);
+
+        Map in = deser(out,
+//                Map.class
+                new TypeToken<Map<String, ObjWithIdentityInfoAnnotation>>() {}
+                );
+        ObjWithIdentityInfoAnnotation a = (ObjWithIdentityInfoAnnotation)in.get("a");
+        ObjWithIdentityInfoAnnotation b = (ObjWithIdentityInfoAnnotation)in.get("b");
+        ObjWithIdentityInfoAnnotation c = (ObjWithIdentityInfoAnnotation)in.get("c");
+        Asserts.assertTrue(a.foo.equals(c.foo), "expected same foo value for a and c - "+a+" != "+c);
+        Asserts.assertTrue(!b.foo.equals(c.foo), "expected different foo value for a and b");
+        Asserts.assertTrue(a == c, "expected same instance for a and c - "+a+" != "+c);
+        Asserts.assertTrue(a != b, "expected different instance for a and b");
+    }
+
+    @Test
+    public void testCustomHandlerForReferences() throws Exception {
+        mapper = new ObjectReferencingSerialization().useMapper(
+                BeanWithTypeUtils.applyCommonMapperConfig(
+                    YAMLMapper.builder()
+//                        .handlerInstantiator(new AllBeansIdentityHandler())
+                        .build()
+                , null, false, null, true));
+
+        ObjWithoutIdentityInfoAnnotation f1 = new ObjWithoutIdentityInfoAnnotation(); f1.foo = "1";
+        ObjWithoutIdentityInfoAnnotation f2 = new ObjWithoutIdentityInfoAnnotation(); f2.foo = "2";
+        String out = ser(MutableMap.of("a", f1, "b", f2, "c", f1));
+        LOG.info("Result of "+ JavaClassNames.niceClassAndMethod()+": "+out);
+
+        Map in = deser(out,
+//                Map.class
+                new TypeToken<Map<String, ObjWithoutIdentityInfoAnnotation>>() {}
+        );
+        ObjWithoutIdentityInfoAnnotation a = (ObjWithoutIdentityInfoAnnotation)in.get("a");
+        ObjWithoutIdentityInfoAnnotation b = (ObjWithoutIdentityInfoAnnotation)in.get("b");
+        ObjWithoutIdentityInfoAnnotation c = (ObjWithoutIdentityInfoAnnotation)in.get("c");
+        Asserts.assertTrue(a.foo.equals(c.foo), "expected same foo value for a and c - "+a+" != "+c);
+        Asserts.assertTrue(!b.foo.equals(c.foo), "expected different foo value for a and b");
+        Asserts.assertTrue(a == c, "expected same instance for a and c - "+a+" != "+c);
+        Asserts.assertTrue(a != b, "expected different instance for a and b");
+    }
 
 }
diff --git a/core/src/test/java/org/apache/brooklyn/core/resolve/jackson/MapperTestFixture.java b/core/src/test/java/org/apache/brooklyn/core/resolve/jackson/MapperTestFixture.java
index c92ef37..387ee77 100644
--- a/core/src/test/java/org/apache/brooklyn/core/resolve/jackson/MapperTestFixture.java
+++ b/core/src/test/java/org/apache/brooklyn/core/resolve/jackson/MapperTestFixture.java
@@ -21,6 +21,7 @@ package org.apache.brooklyn.core.resolve.jackson;
 import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.google.common.collect.Iterables;
+import com.google.common.reflect.TypeToken;
 import org.apache.brooklyn.api.typereg.RegisteredType;
 import org.apache.brooklyn.util.core.task.BasicExecutionContext;
 import org.apache.brooklyn.util.core.task.BasicExecutionManager;
@@ -46,6 +47,14 @@ public interface MapperTestFixture {
         }
     }
 
+    default <T> String ser(T v, TypeToken<T> type) {
+        try {
+            return mapper().writerFor(BrooklynJacksonType.asTypeReference(type)).writeValueAsString(v);
+        } catch (JsonProcessingException e) {
+            throw Exceptions.propagate(e);
+        }
+    }
+
     default <T> T deser(String v, Class<T> type) {
         try {
             return mapper().readValue(v, type);
@@ -54,6 +63,15 @@ public interface MapperTestFixture {
         }
     }
 
+
+    default <T> T deser(String v, TypeToken<T> type) {
+        try {
+            return mapper().readValue(v, BrooklynJacksonType.asTypeReference(type));
+        } catch (JsonProcessingException e) {
+            throw Exceptions.propagate(e);
+        }
+    }
+
     default <T> T deser(String v, RegisteredType type) {
         try {
             return mapper().readValue(v, BrooklynJacksonType.of(type));
diff --git a/core/src/test/java/org/apache/brooklyn/util/core/internal/TypeCoercionsTest.java b/core/src/test/java/org/apache/brooklyn/util/core/internal/TypeCoercionsTest.java
index 5e07c1f..1a81ae9 100644
--- a/core/src/test/java/org/apache/brooklyn/util/core/internal/TypeCoercionsTest.java
+++ b/core/src/test/java/org/apache/brooklyn/util/core/internal/TypeCoercionsTest.java
@@ -18,6 +18,7 @@
  */
 package org.apache.brooklyn.util.core.internal;
 
+import org.apache.brooklyn.test.Asserts;
 import static org.testng.Assert.assertEquals;
 import static org.testng.Assert.assertNull;
 import static org.testng.Assert.assertTrue;
@@ -421,4 +422,18 @@ public class TypeCoercionsTest {
     
     public static class MyClazz implements MyInterface {
     }
+
+    public static class ClassWithMap {
+        Map<String,Object> properties = MutableMap.of();
+    }
+
+    @Test
+    public void testObjectInMapCoercion() {
+        ClassWithMap r1 = TypeCoercions.coerce(MutableMap.of("properties", MutableMap.of("x", 1)), ClassWithMap.class);
+        Assert.assertEquals(r1.properties.get("x"), 1);
+
+        r1 = TypeCoercions.coerce(MutableMap.of("properties", MutableMap.of("x", new MyClazz())), ClassWithMap.class);
+        Asserts.assertInstanceOf(r1.properties.get("x"), MyClazz.class);
+    }
+
 }