diff --git a/kotlin/tests/src/test/kotlin/com/squareup/moshi/kotlin/reflect/KotlinJsonAdapterTest.kt b/kotlin/tests/src/test/kotlin/com/squareup/moshi/kotlin/reflect/KotlinJsonAdapterTest.kt index 362943f..6d2c1ea 100644 --- a/kotlin/tests/src/test/kotlin/com/squareup/moshi/kotlin/reflect/KotlinJsonAdapterTest.kt +++ b/kotlin/tests/src/test/kotlin/com/squareup/moshi/kotlin/reflect/KotlinJsonAdapterTest.kt @@ -30,6 +30,8 @@ import org.assertj.core.api.Assertions.assertThat import org.junit.Assert.fail import org.junit.Test import java.io.ByteArrayOutputStream +import java.lang.reflect.ParameterizedType +import java.lang.reflect.WildcardType import java.util.Locale import java.util.SimpleTimeZone import kotlin.annotation.AnnotationRetention.RUNTIME @@ -982,4 +984,144 @@ class KotlinJsonAdapterTest { } class PlainKotlinClass + + @Test fun mapOfStringToStandardReflectionWildcards() { + mapWildcardsParameterizedTest( + MapOfStringToStandardReflection::class.java, + """{"map":{"key":"value"}}""", + MapOfStringToStandardReflection(mapOf("key" to "value"))) + } + + @JvmSuppressWildcards(suppress = false) + data class MapOfStringToStandardReflection(val map: Map = mapOf()) + + @Test fun mapOfStringToStandardCodegenWildcards() { + mapWildcardsParameterizedTest( + MapOfStringToStandardCodegen::class.java, + """{"map":{"key":"value"}}""", + MapOfStringToStandardCodegen(mapOf("key" to "value"))) + } + + @JsonClass(generateAdapter = true) + @JvmSuppressWildcards(suppress = false) + data class MapOfStringToStandardCodegen(val map: Map = mapOf()) + + @Test fun mapOfStringToEnumReflectionWildcards() { + mapWildcardsParameterizedTest( + MapOfStringToEnumReflection::class.java, + """{"map":{"key":"A"}}""", + MapOfStringToEnumReflection(mapOf("key" to KotlinEnum.A))) + } + + @JvmSuppressWildcards(suppress = false) + data class MapOfStringToEnumReflection(val map: Map = mapOf()) + + @Test fun mapOfStringToEnumCodegenWildcards() { + mapWildcardsParameterizedTest( + MapOfStringToEnumCodegen::class.java, + """{"map":{"key":"A"}}""", + MapOfStringToEnumCodegen(mapOf("key" to KotlinEnum.A))) + } + + @JsonClass(generateAdapter = true) + @JvmSuppressWildcards(suppress = false) + data class MapOfStringToEnumCodegen(val map: Map = mapOf()) + + @Test fun mapOfStringToCollectionReflectionWildcards() { + mapWildcardsParameterizedTest( + MapOfStringToCollectionReflection::class.java, + """{"map":{"key":[]}}""", + MapOfStringToCollectionReflection(mapOf("key" to listOf()))) + } + + @JvmSuppressWildcards(suppress = false) + data class MapOfStringToCollectionReflection(val map: Map> = mapOf()) + + @Test fun mapOfStringToCollectionCodegenWildcards() { + mapWildcardsParameterizedTest( + MapOfStringToCollectionCodegen::class.java, + """{"map":{"key":[]}}""", + MapOfStringToCollectionCodegen(mapOf("key" to listOf()))) + } + + @JsonClass(generateAdapter = true) + @JvmSuppressWildcards(suppress = false) + data class MapOfStringToCollectionCodegen(val map: Map> = mapOf()) + + @Test fun mapOfStringToMapReflectionWildcards() { + mapWildcardsParameterizedTest( + MapOfStringToMapReflection::class.java, + """{"map":{"key":{}}}""", + MapOfStringToMapReflection(mapOf("key" to mapOf()))) + } + + @JvmSuppressWildcards(suppress = false) + data class MapOfStringToMapReflection(val map: Map> = mapOf()) + + @Test fun mapOfStringToMapCodegenWildcards() { + mapWildcardsParameterizedTest( + MapOfStringToMapCodegen::class.java, + """{"map":{"key":{}}}""", + MapOfStringToMapCodegen(mapOf("key" to mapOf()))) + } + + @JsonClass(generateAdapter = true) + @JvmSuppressWildcards(suppress = false) + data class MapOfStringToMapCodegen(val map: Map> = mapOf()) + + @Test fun mapOfStringToArrayReflectionWildcards() { + mapWildcardsParameterizedTest( + MapOfStringToArrayReflection::class.java, + """{"map":{"key":[]}}""", + MapOfStringToArrayReflection(mapOf("key" to arrayOf()))) + } + + @JvmSuppressWildcards(suppress = false) + data class MapOfStringToArrayReflection(val map: Map> = mapOf()) + + @Test fun mapOfStringToArrayCodegenWildcards() { + mapWildcardsParameterizedTest( + MapOfStringToArrayCodegen::class.java, + """{"map":{"key":[]}}""", + MapOfStringToArrayCodegen(mapOf("key" to arrayOf()))) + } + + @JsonClass(generateAdapter = true) + @JvmSuppressWildcards(suppress = false) + data class MapOfStringToArrayCodegen(val map: Map> = mapOf()) + + @Test fun mapOfStringToClassReflectionWildcards() { + mapWildcardsParameterizedTest( + MapOfStringToClassReflection::class.java, + """{"map":{"key":{"a":19,"b":42}}}""", + MapOfStringToClassReflection(mapOf("key" to ConstructorParameters(19, 42)))) + } + + @JvmSuppressWildcards(suppress = false) + data class MapOfStringToClassReflection(val map: Map = mapOf()) + + @Test fun mapOfStringToClassCodegenWildcards() { + mapWildcardsParameterizedTest( + MapOfStringToClassCodegen::class.java, + """{"map":{"key":{"a":19,"b":42}}}""", + MapOfStringToClassCodegen(mapOf("key" to ConstructorParameters(19, 42)))) + } + + @JsonClass(generateAdapter = true) + @JvmSuppressWildcards(suppress = false) + data class MapOfStringToClassCodegen(val map: Map = mapOf()) + + private fun mapWildcardsParameterizedTest(type: Class, json: String, value: T) { + // Ensure the map was created with the expected wildcards of a Kotlin map. + val fieldType = type.getDeclaredField("map").genericType + val fieldTypeArguments = (fieldType as ParameterizedType).actualTypeArguments + assertThat(fieldTypeArguments[0]).isNotInstanceOf(WildcardType::class.java) + assertThat(fieldTypeArguments[1]).isInstanceOf(WildcardType::class.java) + + val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build() + val adapter = moshi.adapter(type) + + assertThat(adapter.fromJson(json)).isEqualToComparingFieldByFieldRecursively(value) + assertThat(adapter.toJson(value)).isEqualTo(json) + } } diff --git a/moshi/src/main/java/com/squareup/moshi/Moshi.java b/moshi/src/main/java/com/squareup/moshi/Moshi.java index 7628370..8f8878f 100644 --- a/moshi/src/main/java/com/squareup/moshi/Moshi.java +++ b/moshi/src/main/java/com/squareup/moshi/Moshi.java @@ -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 annotations) { if (annotations == null) throw new NullPointerException("annotations == null"); - type = canonicalize(type); + type = removeSubtypeWildcard(canonicalize(type)); int skipPastIndex = factories.indexOf(skipPast); if (skipPastIndex == -1) { diff --git a/moshi/src/main/java/com/squareup/moshi/internal/Util.java b/moshi/src/main/java/com/squareup/moshi/internal/Util.java index 2aff94c..572c998 100644 --- a/moshi/src/main/java/com/squareup/moshi/internal/Util.java +++ b/moshi/src/main/java/com/squareup/moshi/internal/Util.java @@ -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) { diff --git a/moshi/src/test/java/com/squareup/moshi/MapJsonAdapterTest.java b/moshi/src/test/java/com/squareup/moshi/MapJsonAdapterTest.java index d368ce6..c2aaed5 100644 --- a/moshi/src/test/java/com/squareup/moshi/MapJsonAdapterTest.java +++ b/moshi/src/test/java/com/squareup/moshi/MapJsonAdapterTest.java @@ -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. + JsonAdapter> jsonAdapter = + mapAdapter(String.class, Types.subtypeOf(Object.class)); + + Map 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 map = new LinkedHashMap<>(); map.put("c", 1); diff --git a/moshi/src/test/java/com/squareup/moshi/MoshiTest.java b/moshi/src/test/java/com/squareup/moshi/MoshiTest.java index 41b4492..e1e818a 100644 --- a/moshi/src/test/java/com/squareup/moshi/MoshiTest.java +++ b/moshi/src/test/java/com/squareup/moshi/MoshiTest.java @@ -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 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() {