diff --git a/plugins/kotlinx-serialization/kotlinx-serialization.backend/src/org/jetbrains/kotlinx/serialization/compiler/backend/ir/IrSerializableProperties.kt b/plugins/kotlinx-serialization/kotlinx-serialization.backend/src/org/jetbrains/kotlinx/serialization/compiler/backend/ir/IrSerializableProperties.kt index 1136aedbb25..7f954f05c59 100644 --- a/plugins/kotlinx-serialization/kotlinx-serialization.backend/src/org/jetbrains/kotlinx/serialization/compiler/backend/ir/IrSerializableProperties.kt +++ b/plugins/kotlinx-serialization/kotlinx-serialization.backend/src/org/jetbrains/kotlinx/serialization/compiler/backend/ir/IrSerializableProperties.kt @@ -10,27 +10,25 @@ import org.jetbrains.kotlin.ir.ObsoleteDescriptorBasedAPI import org.jetbrains.kotlin.ir.declarations.IrClass import org.jetbrains.kotlin.ir.declarations.IrProperty import org.jetbrains.kotlin.ir.types.IrSimpleType -import org.jetbrains.kotlin.ir.util.hasAnnotation -import org.jetbrains.kotlin.ir.util.hasDefaultValue -import org.jetbrains.kotlin.ir.util.primaryConstructor -import org.jetbrains.kotlin.ir.util.properties +import org.jetbrains.kotlin.ir.util.* import org.jetbrains.kotlin.name.Name import org.jetbrains.kotlin.serialization.deserialization.descriptors.DeserializedPropertyDescriptor import org.jetbrains.kotlinx.serialization.compiler.extensions.SerializationDescriptorSerializerPlugin -import org.jetbrains.kotlinx.serialization.compiler.extensions.SerializationPluginContext import org.jetbrains.kotlinx.serialization.compiler.resolve.* class IrSerializableProperty( val ir: IrProperty, override val isConstructorParameterWithDefault: Boolean, hasBackingField: Boolean, - declaresDefaultValue: Boolean + declaresDefaultValue: Boolean, + val type: IrSimpleType ) : ISerializableProperty { override val name = ir.annotations.serialNameValue ?: ir.name.asString() override val originalDescriptorName: Name = ir.name - val type = ir.getter!!.returnType as IrSimpleType val genericIndex = type.genericIndex - fun serializableWith(ctx: SerializationBaseContext) = ir.annotations.serializableWith() ?: analyzeSpecialSerializers(ctx, ir.annotations) + fun serializableWith(ctx: SerializationBaseContext) = + ir.annotations.serializableWith() ?: analyzeSpecialSerializers(ctx, ir.annotations) + override val optional = !ir.annotations.hasAnnotation(SerializationAnnotations.requiredAnnotationFqName) && declaresDefaultValue override val transient = ir.annotations.hasAnnotation(SerializationAnnotations.serialTransientFqName) || !hasBackingField } @@ -42,10 +40,27 @@ class IrSerializableProperties( override val serializableStandaloneProperties: List ) : ISerializableProperties +/** + * typeReplacement should be populated from FakeOverrides and is used when we want to determine the type for property + * accounting for generic substitutions performed in subclasses: + * + * ``` + * @Serializable + * sealed class TypedSealedClass(val a: T) { + * @Serializable + * data class Child(val y: Int) : TypedSealedClass("10") + * } + * ``` + * In this case, serializableProperties for TypedSealedClass is a listOf(IrSerProp(val a: T)), + * but for Child is a listOf(IrSerProp(val a: String), IrSerProp(val y: Int)). + * + * Using this approach, we can correctly deserialize parent's properties in Child.Companion.deserialize() + */ @OptIn(ObsoleteDescriptorBasedAPI::class) internal fun serializablePropertiesForIrBackend( irClass: IrClass, - serializationDescriptorSerializer: SerializationDescriptorSerializerPlugin? = null + serializationDescriptorSerializer: SerializationDescriptorSerializerPlugin? = null, + typeReplacement: Map? = null ): IrSerializableProperties { val properties = irClass.properties.toList() val primaryConstructorParams = irClass.primaryConstructor?.valueParameters.orEmpty() @@ -68,14 +83,17 @@ internal fun serializablePropertiesForIrBackend( .map { val isConstructorParameterWithDefault = primaryParamsAsProps[it] ?: false // FIXME: workaround because IrLazyProperty doesn't deserialize information about backing fields. Fallback to descriptor won't work with FIR. - val isPropertyFromAnotherModuleDeclaresDefaultValue = it.descriptor is DeserializedPropertyDescriptor && it.descriptor.declaresDefaultValue() - val isPropertyWithBackingFieldFromAnotherModule = it.descriptor is DeserializedPropertyDescriptor && (it.descriptor.backingField != null || isPropertyFromAnotherModuleDeclaresDefaultValue) + val isPropertyFromAnotherModuleDeclaresDefaultValue = + it.descriptor is DeserializedPropertyDescriptor && it.descriptor.declaresDefaultValue() + val isPropertyWithBackingFieldFromAnotherModule = + it.descriptor is DeserializedPropertyDescriptor && (it.descriptor.backingField != null || isPropertyFromAnotherModuleDeclaresDefaultValue) IrSerializableProperty( it, isConstructorParameterWithDefault, it.backingField != null || isPropertyWithBackingFieldFromAnotherModule, it.backingField?.initializer.let { init -> init != null && !init.expression.isInitializePropertyFromParameter() } || isConstructorParameterWithDefault - || isPropertyFromAnotherModuleDeclaresDefaultValue + || isPropertyFromAnotherModuleDeclaresDefaultValue, + typeReplacement?.get(it) ?: it.getter!!.returnType as IrSimpleType ) } .filterNot { it.transient } @@ -83,13 +101,22 @@ internal fun serializablePropertiesForIrBackend( var serializableProps = run { val supers = irClass.getSuperClassNotAny() - if (supers == null || !supers.isInternalSerializable) + if (supers == null || !supers.isInternalSerializable) { primaryCtorSerializableProps + bodySerializableProps - else + } else { + val originalToTypeFromFO = typeReplacement ?: buildMap { + irClass.properties.filter { it.isFakeOverride }.forEach { prop -> + val orig = prop.resolveFakeOverride() + val type = prop.getter?.returnType as? IrSimpleType + if (orig != null && type != null) put(orig, type) + } + } serializablePropertiesForIrBackend( supers, - serializationDescriptorSerializer + serializationDescriptorSerializer, + originalToTypeFromFO ).serializableProperties + primaryCtorSerializableProps + bodySerializableProps + } } // FIXME: since descriptor from FIR does not have classProto in it(?), this line won't do anything @@ -99,4 +126,4 @@ internal fun serializablePropertiesForIrBackend( irClass.isInternallySerializableEnum() || primaryConstructorParams.size == primaryParamsAsProps.size return IrSerializableProperties(serializableProps, isExternallySerializable, primaryCtorSerializableProps, bodySerializableProps) -} \ No newline at end of file +} diff --git a/plugins/kotlinx-serialization/testData/boxIr/genericBaseClassMultiple.kt b/plugins/kotlinx-serialization/testData/boxIr/genericBaseClassMultiple.kt new file mode 100644 index 00000000000..c46c0987099 --- /dev/null +++ b/plugins/kotlinx-serialization/testData/boxIr/genericBaseClassMultiple.kt @@ -0,0 +1,56 @@ +// TARGET_BACKEND: JVM_IR + +// WITH_STDLIB + +import kotlinx.serialization.* +import kotlinx.serialization.json.* + +@Serializable +abstract class Top { + var top: T? = null +} + +@Serializable +open class Intermediate: Top>() { + var inter: V? = null + override fun toString(): String { + return "Intermediate($top, $inter)" + } +} + +@Serializable +open class Bottom: Intermediate() { + var bot: String? = null + override fun toString(): String { + return "Bottom($top, $inter, $bot)" + } +} + +@Serializable +class Bottom2: Bottom() { + override fun toString(): String { + return "Bottom2($top, $inter, $bot)" + } +} + +@Serializable +data class Full( + val b: Bottom2, + val i: Intermediate +) + +fun box(): String { + val j = Json { ignoreUnknownKeys = true } + val b = Bottom2().apply { + top = listOf("a", "b") + inter = "v" + bot = "bot" + } + val f = Full(b, b) + val encoded = j.encodeToString(f) + if (encoded != """{"b":{"top":["a","b"],"inter":"v","bot":"bot"},"i":{"top":["a","b"],"inter":"v"}}""") return "Encoded: $encoded" + + val decoded = j.decodeFromString(encoded) + if (decoded.toString() != "Full(b=Bottom2([a, b], v, bot), i=Intermediate([a, b], v))") return "Decoded: $decoded" + return "OK" +} diff --git a/plugins/kotlinx-serialization/testData/boxIr/genericBaseClassSimple.kt b/plugins/kotlinx-serialization/testData/boxIr/genericBaseClassSimple.kt new file mode 100644 index 00000000000..766e70d4cf8 --- /dev/null +++ b/plugins/kotlinx-serialization/testData/boxIr/genericBaseClassSimple.kt @@ -0,0 +1,46 @@ +// TARGET_BACKEND: JVM_IR + +// WITH_STDLIB + +import kotlinx.serialization.* +import kotlinx.serialization.json.* + +// From #1264 +@Serializable +sealed class TypedSealedClass(val a: T) { + @Serializable + class Child(val y: Int) : TypedSealedClass("10") { + override fun toString(): String = "Child($a, $y)" + } +} + +// From #KT-43910 +@Serializable +open class ValidatableValue( + var value: T? = null, + var error: V? = null, +) + +@Serializable +class Email : ValidatableValue() { // Note this is a different T + override fun toString(): String { + return "Email($value, $error)" + } +} + +fun box(): String { + val encodedChild = """{"a":"11","y":42}""" + val decodedChild = Json.decodeFromString(encodedChild) + if (decodedChild.toString() != "Child(11, 42)") return "DecodedChild: $decodedChild" + Json.encodeToString(decodedChild)?.let { if (it != encodedChild) return "EncodedChild: $it" } + + val email = Email().apply { + value = "foo" + error = 1 + } + val encodedEmail = Json.encodeToString(email) + if (encodedEmail != """{"value":"foo","error":1}""") return "EncodedEmail: $encodedEmail" + val decodedEmail = Json.decodeFromString>(encodedEmail) + if (decodedEmail.toString() != "Email(foo, 1)") return "DecodedEmail: $decodedEmail" + return "OK" +} \ No newline at end of file diff --git a/plugins/kotlinx-serialization/tests-gen/org/jetbrains/kotlinx/serialization/runners/SerializationFirBlackBoxTestGenerated.java b/plugins/kotlinx-serialization/tests-gen/org/jetbrains/kotlinx/serialization/runners/SerializationFirBlackBoxTestGenerated.java index fe56f076125..456ad14c8e8 100644 --- a/plugins/kotlinx-serialization/tests-gen/org/jetbrains/kotlinx/serialization/runners/SerializationFirBlackBoxTestGenerated.java +++ b/plugins/kotlinx-serialization/tests-gen/org/jetbrains/kotlinx/serialization/runners/SerializationFirBlackBoxTestGenerated.java @@ -51,6 +51,18 @@ public class SerializationFirBlackBoxTestGenerated extends AbstractSerialization runTest("plugins/kotlinx-serialization/testData/boxIr/enumsAreCached.kt"); } + @Test + @TestMetadata("genericBaseClassMultiple.kt") + public void testGenericBaseClassMultiple() throws Exception { + runTest("plugins/kotlinx-serialization/testData/boxIr/genericBaseClassMultiple.kt"); + } + + @Test + @TestMetadata("genericBaseClassSimple.kt") + public void testGenericBaseClassSimple() throws Exception { + runTest("plugins/kotlinx-serialization/testData/boxIr/genericBaseClassSimple.kt"); + } + @Test @TestMetadata("generics.kt") public void testGenerics() throws Exception { diff --git a/plugins/kotlinx-serialization/tests-gen/org/jetbrains/kotlinx/serialization/runners/SerializationIrBoxTestGenerated.java b/plugins/kotlinx-serialization/tests-gen/org/jetbrains/kotlinx/serialization/runners/SerializationIrBoxTestGenerated.java index 2425cdec948..fd346155a19 100644 --- a/plugins/kotlinx-serialization/tests-gen/org/jetbrains/kotlinx/serialization/runners/SerializationIrBoxTestGenerated.java +++ b/plugins/kotlinx-serialization/tests-gen/org/jetbrains/kotlinx/serialization/runners/SerializationIrBoxTestGenerated.java @@ -49,6 +49,18 @@ public class SerializationIrBoxTestGenerated extends AbstractSerializationIrBoxT runTest("plugins/kotlinx-serialization/testData/boxIr/enumsAreCached.kt"); } + @Test + @TestMetadata("genericBaseClassMultiple.kt") + public void testGenericBaseClassMultiple() throws Exception { + runTest("plugins/kotlinx-serialization/testData/boxIr/genericBaseClassMultiple.kt"); + } + + @Test + @TestMetadata("genericBaseClassSimple.kt") + public void testGenericBaseClassSimple() throws Exception { + runTest("plugins/kotlinx-serialization/testData/boxIr/genericBaseClassSimple.kt"); + } + @Test @TestMetadata("generics.kt") public void testGenerics() throws Exception {