Support covariant Map values, which are used by Kotlin

This commit is contained in:
Sye van der Veen
2019-04-30 11:12:04 -04:00
parent 0943ef5a61
commit 7e417840e2
5 changed files with 187 additions and 10 deletions

View File

@@ -34,6 +34,7 @@ import javax.annotation.CheckReturnValue;
import javax.annotation.Nullable;
import static com.squareup.moshi.internal.Util.canonicalize;
import static com.squareup.moshi.internal.Util.removeSubtypeWildcard;
import static com.squareup.moshi.internal.Util.typeAnnotatedWithAnnotations;
/**
@@ -112,7 +113,7 @@ public final class Moshi {
throw new NullPointerException("annotations == null");
}
type = canonicalize(type);
type = removeSubtypeWildcard(canonicalize(type));
// If there's an equivalent adapter in the cache, we're done!
Object cacheKey = cacheKey(type, annotations);
@@ -158,7 +159,7 @@ public final class Moshi {
Set<? extends Annotation> annotations) {
if (annotations == null) throw new NullPointerException("annotations == null");
type = canonicalize(type);
type = removeSubtypeWildcard(canonicalize(type));
int skipPastIndex = factories.indexOf(skipPast);
if (skipPastIndex == -1) {

View File

@@ -139,6 +139,21 @@ public final class Util {
}
}
/**
* If type is a "? extends X" wildcard, returns X; otherwise returns type unchanged.
*/
public static Type removeSubtypeWildcard(Type type) {
if (!(type instanceof WildcardType)) return type;
Type[] lowerBounds = ((WildcardType) type).getLowerBounds();
if (lowerBounds.length != 0) return type;
Type[] upperBounds = ((WildcardType) type).getUpperBounds();
if (upperBounds.length != 1) throw new IllegalArgumentException();
return upperBounds[0];
}
public static Type resolve(Type context, Class<?> contextRawType, Type toResolve) {
// This implementation is made a little more complicated in an attempt to avoid object-creation.
while (true) {

View File

@@ -86,6 +86,27 @@ public final class MapJsonAdapterTest {
assertThat(jsonAdapter.fromJson(jsonReader)).isEqualTo(null);
}
@Test public void covariantValue() throws Exception {
// Important for Kotlin maps, which are all Map<K, ? extends T>.
JsonAdapter<Map<String, Object>> jsonAdapter =
mapAdapter(String.class, Types.subtypeOf(Object.class));
Map<String, Object> map = new LinkedHashMap<>();
map.put("boolean", true);
map.put("float", 42.0);
map.put("String", "value");
String asJson = "{\"boolean\":true,\"float\":42.0,\"String\":\"value\"}";
Buffer buffer = new Buffer();
JsonWriter jsonWriter = JsonWriter.of(buffer);
jsonAdapter.toJson(jsonWriter, map);
assertThat(buffer.readUtf8()).isEqualTo(asJson);
JsonReader jsonReader = newReader(asJson);
assertThat(jsonAdapter.fromJson(jsonReader)).isEqualTo(map);
}
@Test public void orderIsRetained() throws Exception {
Map<String, Integer> map = new LinkedHashMap<>();
map.put("c", 1);

View File

@@ -537,15 +537,13 @@ public final class MoshiTest {
assertThat(adapter.toJson(null)).isEqualTo("null");
}
@Test public void upperBoundedWildcardsAreNotHandled() {
@Test public void upperBoundedWildcardsAreHandled() throws Exception {
Moshi moshi = new Moshi.Builder().build();
try {
moshi.adapter(Types.subtypeOf(String.class));
fail();
} catch (IllegalArgumentException e) {
assertThat(e).hasMessage(
"No JsonAdapter for ? extends java.lang.String (with no annotations)");
}
JsonAdapter<Object> adapter = moshi.adapter(Types.subtypeOf(String.class));
assertThat(adapter.fromJson("\"a\"")).isEqualTo("a");
assertThat(adapter.toJson("b")).isEqualTo("\"b\"");
assertThat(adapter.fromJson("null")).isEqualTo(null);
assertThat(adapter.toJson(null)).isEqualTo("null");
}
@Test public void lowerBoundedWildcardsAreNotHandled() {