Repository: google/gson (Java)
Date: 2022-02-03 23:01:50+00:00
Task type: Bug Fix

Issue:
`EnumMap` deserialization fails with `ClassCastException`
# Gson version
e2e851c9bc692cec68ba7b0cbb002f82b4a229e4

# Java / Android version
openjdk version "11.0.13" 2021-10-19
OpenJDK Runtime Environment Temurin-11.0.13+8 (build 11.0.13+8)
OpenJDK 64-Bit Server VM Temurin-11.0.13+8 (build 11.0.13+8, mixed mode)

# Description
`EnumMap` deserialization fails with `ClassCastException`:
```
java.lang.ClassCastException: class java.util.LinkedHashMap cannot be cast to class java.util.EnumMap
```

The underlying issue is related to #1708, it appears special handling for `EnumMap` is missing.
(I am a bit surprised that this issue has not been mentioned here anywhere before.)

# Reproduction steps
Test case:
```java
private static enum MyEnum {
  VALUE1, VALUE2
}

public void testEnumMap() throws Exception {
  EnumMap<MyEnum, String> map = new EnumMap<MyEnum, String>(MyEnum.class);
  map.put(MyEnum.VALUE1, "test");
  String json = gson.toJson(map);
  assertEquals("{\"VALUE1\":\"test\"}", json);

  Type type = new TypeToken<EnumMap<MyEnum, String>>() {}.getType();
  EnumMap<?, ?> actualMap = gson.fromJson("{\"VALUE1\":\"test\"}", type);
  Map<?, ?> expectedMap = Collections.singletonMap(MyEnum.VALUE1, "test");
  assertEquals(expectedMap, actualMap);
}
```

# Exception stack trace
Not really useful because `ClassCastException` occurs in user code.



Issue discussion:

Solution patch:
diff --git a/gson/src/main/java/com/google/gson/internal/ConstructorConstructor.java b/gson/src/main/java/com/google/gson/internal/ConstructorConstructor.java
--- a/gson/src/main/java/com/google/gson/internal/ConstructorConstructor.java
+++ b/gson/src/main/java/com/google/gson/internal/ConstructorConstructor.java
@@ -23,6 +23,7 @@
 import java.util.ArrayDeque;
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.EnumMap;
 import java.util.EnumSet;
 import java.util.LinkedHashMap;
 import java.util.LinkedHashSet;
@@ -199,7 +200,26 @@ private <T> ObjectConstructor<T> newDefaultImplementationConstructor(
     }
 
     if (Map.class.isAssignableFrom(rawType)) {
-      if (ConcurrentNavigableMap.class.isAssignableFrom(rawType)) {
+      // Only support creation of EnumMap, but not of custom subtypes; for them type parameters
+      // and constructor parameter might have completely different meaning
+      if (rawType == EnumMap.class) {
+        return new ObjectConstructor<T>() {
+          @Override public T construct() {
+            if (type instanceof ParameterizedType) {
+              Type elementType = ((ParameterizedType) type).getActualTypeArguments()[0];
+              if (elementType instanceof Class) {
+                @SuppressWarnings("rawtypes")
+                T map = (T) new EnumMap((Class) elementType);
+                return map;
+              } else {
+                throw new JsonIOException("Invalid EnumMap type: " + type.toString());
+              }
+            } else {
+              throw new JsonIOException("Invalid EnumMap type: " + type.toString());
+            }
+          }
+        };
+      } else if (ConcurrentNavigableMap.class.isAssignableFrom(rawType)) {
         return new ObjectConstructor<T>() {
           @Override public T construct() {
             return (T) new ConcurrentSkipListMap<Object, Object>();


Test patch:
diff --git a/gson/src/test/java/com/google/gson/functional/EnumTest.java b/gson/src/test/java/com/google/gson/functional/EnumTest.java
--- a/gson/src/test/java/com/google/gson/functional/EnumTest.java
+++ b/gson/src/test/java/com/google/gson/functional/EnumTest.java
@@ -31,7 +31,10 @@
 import java.lang.reflect.Type;
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.Collections;
+import java.util.EnumMap;
 import java.util.EnumSet;
+import java.util.Map;
 import java.util.Set;
 import junit.framework.TestCase;
 /**
@@ -150,6 +153,8 @@ public void testEnumCaseMapping() {
   public void testEnumSet() {
     EnumSet<Roshambo> foo = EnumSet.of(Roshambo.ROCK, Roshambo.PAPER);
     String json = gson.toJson(foo);
+    assertEquals("[\"ROCK\",\"PAPER\"]", json);
+
     Type type = new TypeToken<EnumSet<Roshambo>>() {}.getType();
     EnumSet<Roshambo> bar = gson.fromJson(json, type);
     assertTrue(bar.contains(Roshambo.ROCK));
@@ -157,6 +162,18 @@ public void testEnumSet() {
     assertFalse(bar.contains(Roshambo.SCISSORS));
   }
 
+  public void testEnumMap() throws Exception {
+    EnumMap<MyEnum, String> map = new EnumMap<MyEnum, String>(MyEnum.class);
+    map.put(MyEnum.VALUE1, "test");
+    String json = gson.toJson(map);
+    assertEquals("{\"VALUE1\":\"test\"}", json);
+
+    Type type = new TypeToken<EnumMap<MyEnum, String>>() {}.getType();
+    EnumMap<?, ?> actualMap = gson.fromJson("{\"VALUE1\":\"test\"}", type);
+    Map<?, ?> expectedMap = Collections.singletonMap(MyEnum.VALUE1, "test");
+    assertEquals(expectedMap, actualMap);
+  }
+
   public enum Roshambo {
     ROCK {
       @Override Roshambo defeats() {