Repository: google/gson (Java)
Date: 2022-07-25 19:12:13+00:00
Task type: Bug Fix

Issue:
Gson.getAdapter((TypeToken) null) throws exception
The `Gson` class has logic for handling `null` arguments for `getAdapter(TypeToken)`, see:
https://github.com/google/gson/blob/ceae88bd6667f4263bbe02e6b3710b8a683906a2/gson/src/main/java/com/google/gson/Gson.java#L434

However, it appears the pull request which tried to keep the support for `null` arguments also broke it due to commit 31dcfa3ad6fff0ee64f0fc5b8a1712c3ef3dcf95 because it does not reassign a non-`null` value to the `type` argument anymore which later on in the method causes a `NullPointerException`.

However that was 4 years ago and it appears support for `null` arguments might not have been used much (if at all; I did not find existing issues about this here on GitHub). Therefore maybe it would be better instead of fixing this issue to simply remove `null` handling for `getAdapter(TypeToken)` completely and to throw an exception on purpose.


Issue discussion:

Solution patch:
diff --git a/gson/src/main/java/com/google/gson/Gson.java b/gson/src/main/java/com/google/gson/Gson.java
--- a/gson/src/main/java/com/google/gson/Gson.java
+++ b/gson/src/main/java/com/google/gson/Gson.java
@@ -55,6 +55,7 @@
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
 import java.util.concurrent.atomic.AtomicLong;
 import java.util.concurrent.atomic.AtomicLongArray;
 
@@ -143,7 +144,6 @@ public final class Gson {
   static final ToNumberStrategy DEFAULT_OBJECT_TO_NUMBER_STRATEGY = ToNumberPolicy.DOUBLE;
   static final ToNumberStrategy DEFAULT_NUMBER_TO_NUMBER_STRATEGY = ToNumberPolicy.LAZILY_PARSED_NUMBER;
 
-  private static final TypeToken<?> NULL_KEY_SURROGATE = TypeToken.get(Object.class);
   private static final String JSON_NON_EXECUTABLE_PREFIX = ")]}'\n";
 
   /**
@@ -156,7 +156,7 @@ public final class Gson {
   private final ThreadLocal<Map<TypeToken<?>, FutureTypeAdapter<?>>> calls
       = new ThreadLocal<>();
 
-  private final Map<TypeToken<?>, TypeAdapter<?>> typeTokenCache = new ConcurrentHashMap<>();
+  private final ConcurrentMap<TypeToken<?>, TypeAdapter<?>> typeTokenCache = new ConcurrentHashMap<>();
 
   private final ConstructorConstructor constructorConstructor;
   private final JsonAdapterAnnotationTypeAdapterFactory jsonAdapterFactory;
@@ -504,7 +504,10 @@ private static TypeAdapter<AtomicLongArray> atomicLongArrayAdapter(final TypeAda
    */
   @SuppressWarnings("unchecked")
   public <T> TypeAdapter<T> getAdapter(TypeToken<T> type) {
-    TypeAdapter<?> cached = typeTokenCache.get(type == null ? NULL_KEY_SURROGATE : type);
+    if (type == null) {
+      throw new NullPointerException("type must not be null");
+    }
+    TypeAdapter<?> cached = typeTokenCache.get(type);
     if (cached != null) {
       return (TypeAdapter<T>) cached;
     }
@@ -530,8 +533,13 @@ public <T> TypeAdapter<T> getAdapter(TypeToken<T> type) {
       for (TypeAdapterFactory factory : factories) {
         TypeAdapter<T> candidate = factory.create(this, type);
         if (candidate != null) {
+          TypeAdapter<T> existingAdapter = (TypeAdapter<T>) typeTokenCache.putIfAbsent(type, candidate);
+          // If other thread concurrently added adapter prefer that one instead
+          if (existingAdapter != null) {
+            candidate = existingAdapter;
+          }
+
           call.setDelegate(candidate);
-          typeTokenCache.put(type, candidate);
           return candidate;
         }
       }


Test patch:
diff --git a/gson/src/test/java/com/google/gson/GsonTest.java b/gson/src/test/java/com/google/gson/GsonTest.java
--- a/gson/src/test/java/com/google/gson/GsonTest.java
+++ b/gson/src/test/java/com/google/gson/GsonTest.java
@@ -17,6 +17,7 @@
 package com.google.gson;
 
 import com.google.gson.internal.Excluder;
+import com.google.gson.reflect.TypeToken;
 import com.google.gson.stream.JsonReader;
 import com.google.gson.stream.JsonWriter;
 import com.google.gson.stream.MalformedJsonException;
@@ -29,6 +30,7 @@
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
+import java.util.concurrent.atomic.AtomicReference;
 import junit.framework.TestCase;
 
 /**
@@ -89,6 +91,67 @@ private static final class TestTypeAdapter extends TypeAdapter<Object> {
     @Override public Object read(JsonReader in) throws IOException { return null; }
   }
 
+  public void testGetAdapter_Null() {
+    Gson gson = new Gson();
+    try {
+      gson.getAdapter((TypeToken<?>) null);
+      fail();
+    } catch (NullPointerException e) {
+      assertEquals("type must not be null", e.getMessage());
+    }
+  }
+
+  public void testGetAdapter_Concurrency() {
+    final AtomicReference<TypeAdapter<?>> threadAdapter = new AtomicReference<>();
+    final Class<?> requestedType = Number.class;
+
+    Gson gson = new GsonBuilder()
+        .registerTypeAdapterFactory(new TypeAdapterFactory() {
+          private volatile boolean isFirstCall = true;
+
+          @Override public <T> TypeAdapter<T> create(final Gson gson, TypeToken<T> type) {
+            if (isFirstCall) {
+              isFirstCall = false;
+
+              // Create a separate thread which requests an adapter for the same type
+              // This will cause this factory to return a different adapter instance than
+              // the one it is currently creating
+              Thread thread = new Thread() {
+                @Override public void run() {
+                  threadAdapter.set(gson.getAdapter(requestedType));
+                }
+              };
+              thread.start();
+              try {
+                thread.join();
+              } catch (InterruptedException e) {
+                throw new RuntimeException(e);
+              }
+            }
+
+            // Create a new dummy adapter instance
+
+            @SuppressWarnings("unchecked")
+            TypeAdapter<T> r = (TypeAdapter<T>) new TypeAdapter<Number>() {
+              @Override public void write(JsonWriter out, Number value) throws IOException {
+                throw new AssertionError("not needed for test");
+              }
+
+              @Override public Number read(JsonReader in) throws IOException {
+                throw new AssertionError("not needed for test");
+              }
+            };
+            return r;
+          }
+        })
+        .create();
+
+    TypeAdapter<?> adapter = gson.getAdapter(requestedType);
+    assertNotNull(adapter);
+    // Should be the same adapter instance the concurrent thread received
+    assertSame(threadAdapter.get(), adapter);
+  }
+
   public void testNewJsonWriter_Default() throws IOException {
     StringWriter writer = new StringWriter();
     JsonWriter jsonWriter = new Gson().newJsonWriter(writer);