Issue #283: Deserializing Map with enum keys results in runtime string keys (#509) (#522)

Signed-off-by: rmartinc <rmartinc@redhat.com>
diff --git a/src/main/java/org/eclipse/yasson/internal/serializer/AbstractArrayDeserializer.java b/src/main/java/org/eclipse/yasson/internal/serializer/AbstractArrayDeserializer.java
index f9f8ac6..ec27530 100644
--- a/src/main/java/org/eclipse/yasson/internal/serializer/AbstractArrayDeserializer.java
+++ b/src/main/java/org/eclipse/yasson/internal/serializer/AbstractArrayDeserializer.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2016, 2019 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2016, 2021 Oracle and/or its affiliates. All rights reserved.
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Public License v. 2.0 which is available at
@@ -67,7 +67,7 @@
     }
 
     @Override
-    public void appendResult(Object result) {
+    public void appendResult(Object result, Unmarshaller context) {
         appendCaptor(convertNullToOptionalEmpty(componentClass, result));
     }
 
@@ -80,7 +80,7 @@
     protected void deserializeNext(JsonParser parser, Unmarshaller context) {
         final JsonbDeserializer<?> deserializer = newUnmarshallerItemBuilder(context.getJsonbContext()).withType(componentClass)
                 .withCustomization(componentClassModel == null ? null : componentClassModel.getClassCustomization()).build();
-        appendResult(deserializer.deserialize(parser, context, componentClass));
+        appendResult(deserializer.deserialize(parser, context, componentClass), context);
     }
 
     /**
diff --git a/src/main/java/org/eclipse/yasson/internal/serializer/AbstractContainerDeserializer.java b/src/main/java/org/eclipse/yasson/internal/serializer/AbstractContainerDeserializer.java
index 960d42b..065b767 100644
--- a/src/main/java/org/eclipse/yasson/internal/serializer/AbstractContainerDeserializer.java
+++ b/src/main/java/org/eclipse/yasson/internal/serializer/AbstractContainerDeserializer.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2016, 2019 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2016, 2021 Oracle and/or its affiliates. All rights reserved.
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Public License v. 2.0 which is available at
@@ -104,7 +104,7 @@
             case KEY_NAME:
                 break;
             case VALUE_NULL:
-                appendResult(null);
+                appendResult(null, context);
                 break;
             case END_OBJECT:
             case END_ARRAY:
@@ -190,8 +190,9 @@
      * or other embedded objects use methods provided.
      *
      * @param result An instance result of an item.
+     * @param context Current unmarshalling context.
      */
-    public abstract void appendResult(Object result);
+    public abstract void appendResult(Object result, Unmarshaller context);
 
     /**
      * Returns parser context.
diff --git a/src/main/java/org/eclipse/yasson/internal/serializer/AbstractJsonpDeserializer.java b/src/main/java/org/eclipse/yasson/internal/serializer/AbstractJsonpDeserializer.java
index 8d6d597..c85c2c1 100644
--- a/src/main/java/org/eclipse/yasson/internal/serializer/AbstractJsonpDeserializer.java
+++ b/src/main/java/org/eclipse/yasson/internal/serializer/AbstractJsonpDeserializer.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2016, 2019 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2016, 2021 Oracle and/or its affiliates. All rights reserved.
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Public License v. 2.0 which is available at
@@ -47,7 +47,7 @@
     }
 
     @Override
-    public void appendResult(Object result) {
+    public void appendResult(Object result, Unmarshaller context) {
         throw new UnsupportedOperationException("Inner json structures are deserialized by JsonParser.");
     }
 }
diff --git a/src/main/java/org/eclipse/yasson/internal/serializer/CollectionDeserializer.java b/src/main/java/org/eclipse/yasson/internal/serializer/CollectionDeserializer.java
index 838a0a2..a1c4db6 100644
--- a/src/main/java/org/eclipse/yasson/internal/serializer/CollectionDeserializer.java
+++ b/src/main/java/org/eclipse/yasson/internal/serializer/CollectionDeserializer.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2015, 2019 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2015, 2021 Oracle and/or its affiliates. All rights reserved.
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Public License v. 2.0 which is available at
@@ -102,7 +102,7 @@
     }
 
     @Override
-    public void appendResult(Object result) {
+    public void appendResult(Object result, Unmarshaller context) {
         appendCaptor(convertNullToOptionalEmpty(collectionValueType, result));
     }
 
@@ -114,7 +114,7 @@
     @Override
     protected void deserializeNext(JsonParser parser, Unmarshaller context) {
         final JsonbDeserializer<?> deserializer = newCollectionOrMapItem(collectionValueType, context.getJsonbContext());
-        appendResult(deserializer.deserialize(parser, context, collectionValueType));
+        appendResult(deserializer.deserialize(parser, context, collectionValueType), context);
     }
 
     @Override
diff --git a/src/main/java/org/eclipse/yasson/internal/serializer/MapDeserializer.java b/src/main/java/org/eclipse/yasson/internal/serializer/MapDeserializer.java
index 51b0269..620aa15 100644
--- a/src/main/java/org/eclipse/yasson/internal/serializer/MapDeserializer.java
+++ b/src/main/java/org/eclipse/yasson/internal/serializer/MapDeserializer.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2015, 2020 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2015, 2021 Oracle and/or its affiliates. All rights reserved.
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Public License v. 2.0 which is available at
@@ -12,6 +12,7 @@
 
 package org.eclipse.yasson.internal.serializer;
 
+import java.io.StringReader;
 import java.lang.reflect.ParameterizedType;
 import java.lang.reflect.Type;
 import java.util.HashMap;
@@ -33,15 +34,22 @@
 
 /**
  * Item implementation for {@link java.util.Map} fields.
- * According to JSON specification object can have only string keys, given that maps could only be parsed
- * from JSON objects, implementation is bound to String type.
+ * According to JSON specification object can have only string keys.
+ * Nevertheless the implementation lets the key be a basic object that was
+ * serialized into a string representation. Therefore the key is also parsed to
+ * convert it into its parametrized type.
  *
  * @param <T> map type
  */
 public class MapDeserializer<T extends Map<?, ?>> extends AbstractContainerDeserializer<T> implements EmbeddedItem {
 
     /**
-     * Type of value in the map. (Keys must always be Strings, because of JSON spec)
+     * Type of the key in the map.
+     */
+    private final Type mapKeyRuntimeType;
+
+    /**
+     * Type of value in the map.
      */
     private final Type mapValueRuntimeType;
 
@@ -54,6 +62,9 @@
      */
     protected MapDeserializer(DeserializerBuilder builder) {
         super(builder);
+        mapKeyRuntimeType = getRuntimeType() instanceof ParameterizedType
+                ? ReflectionUtils.resolveType(this, ((ParameterizedType) getRuntimeType()).getActualTypeArguments()[0])
+                : Object.class;
         mapValueRuntimeType = getRuntimeType() instanceof ParameterizedType
                 ? ReflectionUtils.resolveType(this, ((ParameterizedType) getRuntimeType()).getActualTypeArguments()[1])
                 : Object.class;
@@ -93,19 +104,23 @@
     }
 
     @Override
-    public void appendResult(Object result) {
-        appendCaptor(getParserContext().getLastKeyName(), convertNullToOptionalEmpty(mapValueRuntimeType, result));
+    public void appendResult(Object result, Unmarshaller context) {
+        // try to deserialize the string key into its type, JaxbException if not possible
+        final Object key = context.deserialize(mapKeyRuntimeType, new JsonbRiParser(
+                context.getJsonbContext().getJsonProvider().createParser(
+                        new StringReader("\"" + getParserContext().getLastKeyName() + "\""))));
+        appendCaptor(key, convertNullToOptionalEmpty(mapValueRuntimeType, result));
     }
 
     @SuppressWarnings("unchecked")
-    private <V> void appendCaptor(String key, V value) {
-        ((Map<String, V>) getInstance(null)).put(key, value);
+    private <K, V> void appendCaptor(K key, V value) {
+        ((Map<K, V>) getInstance(null)).put(key, value);
     }
 
     @Override
     protected void deserializeNext(JsonParser parser, Unmarshaller context) {
         final JsonbDeserializer<?> deserializer = newCollectionOrMapItem(mapValueRuntimeType, context.getJsonbContext());
-        appendResult(deserializer.deserialize(parser, context, mapValueRuntimeType));
+        appendResult(deserializer.deserialize(parser, context, mapValueRuntimeType), context);
     }
 
     @Override
diff --git a/src/main/java/org/eclipse/yasson/internal/serializer/ObjectDeserializer.java b/src/main/java/org/eclipse/yasson/internal/serializer/ObjectDeserializer.java
index c6e0250..8b155c8 100644
--- a/src/main/java/org/eclipse/yasson/internal/serializer/ObjectDeserializer.java
+++ b/src/main/java/org/eclipse/yasson/internal/serializer/ObjectDeserializer.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2015, 2019 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2015, 2021 Oracle and/or its affiliates. All rights reserved.
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Public License v. 2.0 which is available at
@@ -137,9 +137,10 @@
      * Set populated instance of current object to its unfinished wrapper values map.
      *
      * @param result An instance result of an item.
+     * @param context Current unmarshalling context.
      */
     @Override
-    public void appendResult(Object result) {
+    public void appendResult(Object result, Unmarshaller context) {
         final PropertyModel model = getModel();
         //missing property for null values
         if (model == null) {
diff --git a/src/main/java/org/eclipse/yasson/internal/serializer/UserDeserializerDeserializer.java b/src/main/java/org/eclipse/yasson/internal/serializer/UserDeserializerDeserializer.java
index 76a5996..66b271d 100644
--- a/src/main/java/org/eclipse/yasson/internal/serializer/UserDeserializerDeserializer.java
+++ b/src/main/java/org/eclipse/yasson/internal/serializer/UserDeserializerDeserializer.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2016, 2019 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2016, 2021 Oracle and/or its affiliates. All rights reserved.
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Public License v. 2.0 which is available at
@@ -46,7 +46,7 @@
     }
 
     @Override
-    public void appendResult(Object result) {
+    public void appendResult(Object result, Unmarshaller context) {
         //ignore internal deserialize() call in custom deserializer
     }
 
diff --git a/src/test/java/org/eclipse/yasson/serializers/MapToObjectSerializerTest.java b/src/test/java/org/eclipse/yasson/serializers/MapToObjectSerializerTest.java
index 78ac0b2..8166055 100644
--- a/src/test/java/org/eclipse/yasson/serializers/MapToObjectSerializerTest.java
+++ b/src/test/java/org/eclipse/yasson/serializers/MapToObjectSerializerTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2019, 2021 Oracle and/or its affiliates. All rights reserved.
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Public License v. 2.0 which is available at
@@ -18,10 +18,15 @@
 
 import org.junit.jupiter.api.Test;
 
+import static org.eclipse.yasson.Assertions.shouldFail;
+import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
+import java.math.BigInteger;
 import java.util.EnumMap;
+import java.util.HashMap;
 import java.util.Map;
+import java.util.Objects;
 
 /**
  * Test various use-cases with {@code Map} serializer which
@@ -45,6 +50,57 @@
     }
 
     /**
+     * MapObject to test different parametrized maps.
+     * @param <K> The map key
+     * @param <V>The map value
+     */
+    public static class MapObject<K, V> {
+
+        private Map<K, V> values;
+
+        public MapObject() {
+            this.values = new HashMap<>();
+        }
+
+        public Map<K, V> getValues() {
+            return values;
+        }
+
+        public void setValues(Map<K, V> values) {
+            this.values = values;
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (o instanceof MapObject) {
+                MapObject<?,?> to = (MapObject<?,?>) o;
+                return values.equals(to.values);
+            }
+            return false;
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hashCode(this.values);
+        }
+
+        @Override
+        public String toString() {
+            return values.toString();
+        }
+    }
+
+    public static class MapObjectIntegerString extends MapObject<Integer, String> {};
+
+    public static class MapObjectBigIntegerString extends MapObject<BigInteger, String> {};
+
+    public static class MapObjectEnumString extends MapObject<TestEnum, String> {};
+
+    public static class MapObjectStringString extends MapObject<String, String> {};
+
+    public static class MapObjectBooleanString extends MapObject<Boolean, String> {};
+
+    /**
      * Test serialization of Map with Number keys and String values.
      */
     @Test
@@ -58,4 +114,116 @@
             assertTrue(json.contains(e.name()), "Enumeration not well serialized");
         }
     }
+
+    /**
+     * Test for Integer/String map.
+     */
+    @Test
+    public void testIntegerString() {
+        Jsonb jsonb = JsonbBuilder.create(new JsonbConfig());
+
+        MapObjectIntegerString mapObject = new MapObjectIntegerString();
+        mapObject.getValues().put(12, "twelve");
+        mapObject.getValues().put(48, "forty eight");
+        mapObject.getValues().put(256, "two hundred fifty-six");
+
+        String json = jsonb.toJson(mapObject);
+        MapObjectIntegerString resObject = jsonb.fromJson(json, MapObjectIntegerString.class);
+        assertEquals(mapObject, resObject);
+    }
+
+    /**
+     * Test for BigInteger/String map.
+     */
+    @Test
+    public void testBigIntegerString() {
+        Jsonb jsonb = JsonbBuilder.create(new JsonbConfig());
+
+        MapObjectBigIntegerString mapObject = new MapObjectBigIntegerString();
+        mapObject.getValues().put(new BigInteger("12"), "twelve");
+        mapObject.getValues().put(new BigInteger("48"), "forty eight");
+        mapObject.getValues().put(new BigInteger("256"), "two hundred fifty-six");
+
+        String json = jsonb.toJson(mapObject);
+        MapObjectBigIntegerString resObject = jsonb.fromJson(json, MapObjectBigIntegerString.class);
+        assertEquals(mapObject, resObject);
+    }
+
+    /**
+     * Test for Enum/String map.
+     */
+    @Test
+    public void testEnumString() {
+        Jsonb jsonb = JsonbBuilder.create();
+
+        MapObjectEnumString mapObject = new MapObjectEnumString();
+        mapObject.getValues().put(TestEnum.ONE, "one");
+        mapObject.getValues().put(TestEnum.TWO, "two");
+
+        String json = jsonb.toJson(mapObject);
+        MapObjectEnumString resObject = jsonb.fromJson(json, MapObjectEnumString.class);
+        assertEquals(mapObject, resObject);
+    }
+
+    /**
+     * Test for String/String map.
+     */
+    @Test
+    public void testStringString() {
+        Jsonb jsonb = JsonbBuilder.create(new JsonbConfig().setProperty("lala", "lala"));
+
+        MapObjectStringString mapObject = new MapObjectStringString();
+        mapObject.getValues().put("one", "one");
+        mapObject.getValues().put("two", "two");
+
+        String json = jsonb.toJson(mapObject);
+        MapObjectStringString resObject = jsonb.fromJson(json, MapObjectStringString.class);
+        assertEquals(mapObject, resObject);
+    }
+
+    /**
+     * Test for a non parametrized map that should use Strings as keys.
+     */
+    @Test
+    public void testNotParametrizedMap() {
+        Jsonb jsonb = JsonbBuilder.create(new JsonbConfig());
+
+        Map<Integer, String> mapObject = new HashMap<>();
+        mapObject.put(12, "twelve");
+        mapObject.put(48, "forty eight");
+        mapObject.put(256, "two hundred fifty-six");
+
+        String json = jsonb.toJson(mapObject);
+        Map resObject = jsonb.fromJson(json, Map.class);
+        assertEquals(3, resObject.size());
+        assertTrue(resObject.keySet().iterator().next() instanceof String);
+    }
+
+    /**
+     * Test for Boolean/String map. This map is not even generated by the
+     * MapToObjectSerializer as a boolean is not managed by that serializer.
+     * But the json string should be deserialized in the same way.
+     */
+    @Test
+    public void testBooleanStringMapToObjectSerializer() {
+        Jsonb jsonb = JsonbBuilder.create();
+
+        String json = "{\"values\":{\"true\":\"TRUE\",\"false\":\"FALSE\"}}";
+        MapObjectBooleanString resObject = jsonb.fromJson(json, MapObjectBooleanString.class);
+        assertEquals(2, resObject.getValues().size());
+        assertEquals("TRUE", resObject.getValues().get(true));
+        assertEquals("FALSE", resObject.getValues().get(false));
+    }
+
+    /**
+     * Test for Integer/String map but giving an incorrect integer key.
+     * JsonbException is expected.
+     */
+    @Test
+    public void testIncorrectTypeMapToObjectSerializer() {
+        Jsonb jsonb = JsonbBuilder.create();
+
+        String json = "{\"values\":{\"1\":\"OK\",\"error\":\"KO\"}}";
+        shouldFail(() -> jsonb.fromJson(json, MapObjectIntegerString.class));
+    }
 }