diff --git a/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/metadata.kt b/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/metadata.kt index 32d579f..1f4e9ac 100644 --- a/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/metadata.kt +++ b/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/metadata.kt @@ -52,6 +52,7 @@ import java.lang.annotation.ElementType import java.lang.annotation.Retention import java.lang.annotation.RetentionPolicy import java.lang.annotation.Target +import java.util.TreeSet import javax.annotation.processing.Messager import javax.lang.model.element.ElementKind import javax.lang.model.element.TypeElement @@ -322,10 +323,22 @@ private fun String.escapeDollarSigns(): String { private fun TypeName.unwrapTypeAlias(): TypeName { return when (this) { is ClassName -> { - tag()?.type?.unwrapTypeAlias() ?: this + tag()?.type?.let { unwrappedType -> + // If any type is nullable, then the whole thing is nullable + var isAnyNullable = isNullable + // Keep track of all annotations across type levels. Sort them too for consistency. + val runningAnnotations = TreeSet(compareBy { it.toString() }).apply { + addAll(annotations) + } + val nestedUnwrappedType = unwrappedType.unwrapTypeAlias() + runningAnnotations.addAll(nestedUnwrappedType.annotations) + isAnyNullable = isAnyNullable || nestedUnwrappedType.isNullable + nestedUnwrappedType.copy(nullable = isAnyNullable, annotations = runningAnnotations.toList()) + } ?: this } is ParameterizedTypeName -> { return (rawType.unwrapTypeAlias() as ClassName).parameterizedBy(typeArguments.map { it.unwrapTypeAlias() }) + .copy(nullable = isNullable, annotations = annotations) } is TypeVariableName -> { return copy(bounds = bounds.map { it.unwrapTypeAlias() }) diff --git a/kotlin/tests/src/test/kotlin/com/squareup/moshi/kotlin/DualKotlinTest.kt b/kotlin/tests/src/test/kotlin/com/squareup/moshi/kotlin/DualKotlinTest.kt index 8017ed9..05dd50a 100644 --- a/kotlin/tests/src/test/kotlin/com/squareup/moshi/kotlin/DualKotlinTest.kt +++ b/kotlin/tests/src/test/kotlin/com/squareup/moshi/kotlin/DualKotlinTest.kt @@ -433,7 +433,7 @@ class DualKotlinTest(useReflection: Boolean) { val parameterized: GenericClass, val wildcardIn: GenericClass, val wildcardOut: GenericClass, - val complex: GenericClass + val complex: GenericClass? ) // Regression test for https://github.com/square/moshi/issues/991 @@ -481,11 +481,52 @@ class DualKotlinTest(useReflection: Boolean) { val double: Double, val nullableDouble: Double? = null ) + + // Regression test for https://github.com/square/moshi/issues/990 + @Test fun nullableProperties() { + val adapter = moshi.adapter() + + @Language("JSON") + val testJson = """{"nullableList":null}""" + + assertThat(adapter.serializeNulls().toJson(NullableList(null))) + .isEqualTo(testJson) + + val result = adapter.fromJson(testJson)!! + assertThat(result.nullableList).isNull() + } + + @JsonClass(generateAdapter = true) + data class NullableList(val nullableList: List?) + + @Test fun typeAliasNullability() { + val adapter = moshi.adapter() + + @Language("JSON") + val testJson = """{"aShouldBeNonNull":3,"nullableAShouldBeNullable":null,"redundantNullableAShouldBeNullable":null,"manuallyNullableAShouldBeNullable":null,"convolutedMultiNullableShouldBeNullable":null,"deepNestedNullableShouldBeNullable":null}""" + + val instance = TypeAliasNullability(3, null, null, null, null, null) + assertThat(adapter.serializeNulls().toJson(instance)) + .isEqualTo(testJson) + + val result = adapter.fromJson(testJson)!! + assertThat(result).isEqualTo(instance) + } + + @JsonClass(generateAdapter = true) + data class TypeAliasNullability( + val aShouldBeNonNull: A, + val nullableAShouldBeNullable: NullableA, + val redundantNullableAShouldBeNullable: NullableA?, + val manuallyNullableAShouldBeNullable: A?, + val convolutedMultiNullableShouldBeNullable: NullableB?, + val deepNestedNullableShouldBeNullable: E + ) } typealias TypeAlias = Int @Suppress("REDUNDANT_PROJECTION") -typealias GenericTypeAlias = List> +typealias GenericTypeAlias = List?>? @JsonClass(generateAdapter = true) data class GenericClass(val value: T) @@ -493,3 +534,11 @@ data class GenericClass(val value: T) // Has to be outside since inline classes are only allowed on top level @JsonClass(generateAdapter = true) inline class InlineClass(val i: Int) + +typealias A = Int +typealias NullableA = A? +typealias B = NullableA +typealias NullableB = B? +typealias C = NullableA +typealias D = C +typealias E = D