Skip to content

Commit 80dd2c0

Browse files
committedMar 21, 2025
[KxSerialization] Implemented passing type arguments from container sealed class
Problem described in Kotlin/kotlinx.serialization#2953 (comment) Fixes Kotlin/kotlinx.serialization#2953
1 parent 0d35527 commit 80dd2c0

File tree

5 files changed

+183
-14
lines changed

5 files changed

+183
-14
lines changed
 

‎plugins/kotlinx-serialization/kotlinx-serialization.backend/src/org/jetbrains/kotlinx/serialization/compiler/backend/ir/BaseIrGenerator.kt

+92-14
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,13 @@ import org.jetbrains.kotlin.descriptors.ClassKind
1414
import org.jetbrains.kotlin.descriptors.DescriptorVisibilities
1515
import org.jetbrains.kotlin.ir.UNDEFINED_OFFSET
1616
import org.jetbrains.kotlin.ir.builders.*
17+
import org.jetbrains.kotlin.ir.builders.irGetObject
1718
import org.jetbrains.kotlin.ir.declarations.*
1819
import org.jetbrains.kotlin.ir.expressions.IrClassReference
1920
import org.jetbrains.kotlin.ir.expressions.IrExpression
2021
import org.jetbrains.kotlin.ir.expressions.IrExpressionBody
2122
import org.jetbrains.kotlin.ir.expressions.IrVararg
2223
import org.jetbrains.kotlin.ir.expressions.impl.IrConstructorCallImpl
23-
import org.jetbrains.kotlin.ir.expressions.impl.IrGetEnumValueImpl
2424
import org.jetbrains.kotlin.ir.symbols.IrClassSymbol
2525
import org.jetbrains.kotlin.ir.symbols.IrPropertySymbol
2626
import org.jetbrains.kotlin.ir.symbols.IrSimpleFunctionSymbol
@@ -297,11 +297,11 @@ abstract class BaseIrGenerator(private val currentClass: IrClass, final override
297297
} ?: return null
298298

299299
// workaround for sealed and abstract classes - the `Companion.serializer()` function expects non-null serializers, but does not use them, so serializers of any type can be passed
300-
val replaceArgsWithUnitSerializer = expectedSerializer == polymorphicSerializerId || expectedSerializer == sealedSerializerId
300+
val replaceArgsWithUnitSerializer = expectedSerializer == polymorphicSerializerId
301301

302302
val adjustedArgs: List<IrExpression> =
303303
if (replaceArgsWithUnitSerializer) {
304-
val serializer = findStandardKotlinTypeSerializer(compilerContext, context.irBuiltIns.unitType)!!
304+
val serializer = compilerContext.unitSerializerClass!!
305305
List(baseClass.typeParameters.size) { irGetObject(serializer) }
306306
} else {
307307
args
@@ -479,13 +479,19 @@ abstract class BaseIrGenerator(private val currentClass: IrClass, final override
479479

480480
val kSerializerType = kSerializerType(property.type)
481481

482+
// if in type arguments there are type parameters - we can't cache it in companion's property because she should use actual serializer
483+
val serializers = createSerializerOnlyForClasses(property.type, serializableClass) ?: return null
484+
val genericGetter: (Int, IrType) -> IrExpression = { index, _ ->
485+
serializers[index]
486+
}
487+
482488
val expr = serializerInstance(
483489
serializer,
484490
compilerContext,
485491
property.type,
486492
null,
487493
serializableClass,
488-
null
494+
genericGetter
489495
)
490496

491497
return if (expr != null) {
@@ -497,6 +503,27 @@ abstract class BaseIrGenerator(private val currentClass: IrClass, final override
497503
}
498504
}
499505

506+
// create serializers for type arguments if all arguments are classes, `null` otherwise
507+
private fun IrBuilderWithScope.createSerializerOnlyForClasses(type: IrSimpleType, serializableClass: IrClass): List<IrExpression>? {
508+
// arguments contain star projections or type parameter
509+
if (!type.arguments.all { it is IrSimpleType && it.typeOrNull?.isTypeParameter() != true }) {
510+
return null
511+
}
512+
513+
return type.arguments.map { argumentType ->
514+
argumentType as? IrSimpleType ?: return null
515+
val serializer = findTypeSerializerOrContext(compilerContext, argumentType)
516+
serializerInstance(
517+
serializer,
518+
compilerContext,
519+
argumentType,
520+
null,
521+
serializableClass,
522+
null
523+
) ?: return null // we can't create serializer
524+
}
525+
}
526+
500527
private fun IrSimpleType.checkTypeArgumentsHasSelf(itselfClass: IrClassSymbol): Boolean {
501528
arguments.forEach { typeArgument ->
502529
if (typeArgument.typeOrNull?.classifierOrNull == itselfClass) return true
@@ -618,7 +645,44 @@ abstract class BaseIrGenerator(private val currentClass: IrClass, final override
618645
// instantiate serializer only inside sealed class/interface Companion
619646
if (serializerClassOriginal == kType.classOrUpperBound()?.owner.classSerializer(pluginContext) && this@BaseIrGenerator !is SerializableCompanionIrGenerator) {
620647
// otherwise call Companion.serializer()
621-
callSerializerFromCompanion(kType, typeArgs, emptyList(), sealedSerializerId)?.let { return it }
648+
649+
val args = kType.arguments.map { typeArg ->
650+
val type = typeArg.typeOrNull
651+
when {
652+
type?.isTypeParameter() == true && rootSerializableClass != null -> {
653+
// try to use type argument from root serializable class
654+
val indexInRootClass = typeArg.indexInClass(rootSerializableClass)
655+
serializerInstance(
656+
null,
657+
pluginContext,
658+
type,
659+
indexInRootClass,
660+
rootSerializableClass,
661+
genericGetter
662+
) ?: irGetObject(compilerContext.unitSerializerClass!!)
663+
}
664+
665+
type != null && !type.isTypeParameter() -> {
666+
// create serializer for class type argument
667+
val serializer = findTypeSerializerOrContext(compilerContext, type)
668+
serializerInstance(
669+
serializer,
670+
pluginContext,
671+
type,
672+
null,
673+
rootSerializableClass,
674+
genericGetter
675+
) ?: irGetObject(compilerContext.unitSerializerClass!!)
676+
}
677+
678+
else -> {
679+
// for star projection we can't pick serializer so use Unit serializer
680+
// do the same in other unknown cases
681+
irGetObject(compilerContext.unitSerializerClass!!)
682+
}
683+
}
684+
}
685+
callSerializerFromCompanion(kType, typeArgs, args, sealedSerializerId)?.let { return it }
622686
}
623687

624688
args = mutableListOf<IrExpression>().apply {
@@ -651,18 +715,32 @@ abstract class BaseIrGenerator(private val currentClass: IrClass, final override
651715
pluginContext,
652716
type,
653717
type.genericIndex,
654-
rootSerializableClass
718+
rootSerializableClass,
655719
) { index, genericType ->
656720
val indexInParent = path?.let { mapTypeParameterIndex(index, it) }
657721

658-
if (genericGetter != null && indexInParent != null) {
659-
genericGetter.invoke(indexInParent, genericType)
660-
} else {
661-
serializerInstance(
662-
pluginContext.referenceClass(polymorphicSerializerId),
663-
pluginContext,
664-
(genericType.classifierOrNull as IrTypeParameterSymbol).owner.representativeUpperBound
665-
)!!
722+
when {
723+
genericGetter != null && indexInParent != null -> {
724+
genericGetter.invoke(indexInParent, genericType)
725+
}
726+
!genericType.isTypeParameter() -> {
727+
val serializer = findTypeSerializerOrContext(compilerContext, type)
728+
serializerInstance(
729+
serializer,
730+
pluginContext,
731+
type,
732+
null,
733+
rootSerializableClass,
734+
genericGetter
735+
)!!
736+
}
737+
else -> {
738+
serializerInstance(
739+
pluginContext.referenceClass(polymorphicSerializerId),
740+
pluginContext,
741+
(genericType.classifierOrNull as IrTypeParameterSymbol).owner.representativeUpperBound
742+
)!!
743+
}
666744
}
667745
}!!
668746
wrapWithNullableSerializerIfNeeded(type, expr, nullableSerClass)

‎plugins/kotlinx-serialization/kotlinx-serialization.backend/src/org/jetbrains/kotlinx/serialization/compiler/backend/ir/IrGeneratorUtils.kt

+13
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import org.jetbrains.kotlin.ir.expressions.impl.IrInstanceInitializerCallImpl
1616
import org.jetbrains.kotlin.ir.symbols.FqNameEqualityChecker
1717
import org.jetbrains.kotlin.ir.symbols.IrTypeParameterSymbol
1818
import org.jetbrains.kotlin.ir.types.IrSimpleType
19+
import org.jetbrains.kotlin.ir.types.IrTypeArgument
1920
import org.jetbrains.kotlin.ir.types.getClass
2021
import org.jetbrains.kotlin.ir.util.classId
2122
import org.jetbrains.kotlin.ir.util.primaryConstructor
@@ -123,3 +124,15 @@ private fun IrTypeParameter.belongsClass(typeOfClass: IrSimpleType): Boolean {
123124
return classId != null && classId == classInParameter.classId
124125
}
125126

127+
/**
128+
* Returns index in [serializableClass] of type parameters used as type argument in [this].
129+
*/
130+
internal fun IrTypeArgument.indexInClass(serializableClass: IrClass): Int? {
131+
val rootTypeParameter = serializableClass.typeParameters.firstOrNull { param ->
132+
// TODO is it ok?
133+
param.symbol == (this as? IrSimpleType)?.classifier
134+
}
135+
136+
return rootTypeParameter?.index
137+
}
138+

‎plugins/kotlinx-serialization/kotlinx-serialization.backend/src/org/jetbrains/kotlinx/serialization/compiler/extensions/SerializationLoweringExtension.kt

+3
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,9 @@ class SerializationPluginContext(baseContext: IrPluginContext, val metadataPlugi
108108
*/
109109
internal val kSerializerClass = referenceClass(SerialEntityNames.KSERIALIZER_CLASS_ID)?.owner
110110

111+
internal val unitSerializerClass =
112+
getClassFromRuntimeOrNull("UnitSerializer", SerializationPackages.internalPackageFqName, SerializationPackages.packageFqName)
113+
111114
// evaluated properties
112115
override val runtimeHasEnumSerializerFactoryFunctions = enumSerializerFactoryFunc != null && annotatedEnumSerializerFactoryFunc != null
113116

‎plugins/kotlinx-serialization/testData/boxIr/KeepGeneratedSerializer.kt

+19
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import kotlinx.serialization.*
2727
import kotlinx.serialization.json.*
2828
import kotlinx.serialization.encoding.*
2929
import kotlinx.serialization.descriptors.*
30+
import kotlinx.serialization.builtins.serializer
3031

3132
// == value class
3233
@Serializable(with = ValueSerializer::class)
@@ -214,6 +215,20 @@ public data class Container(
214215
internal object CustomSerializer : KSerializer<Container> by generatedSerializer()
215216
}
216217

218+
@Serializable
219+
public sealed interface ParametrizedSchema<T> {
220+
val x: T
221+
}
222+
223+
@KeepGeneratedSerializer
224+
@Serializable(with = ParametrizedContainer.CustomSerializer::class)
225+
public data class ParametrizedContainer<T>(
226+
val properties: List<ParametrizedSchema<T>>,
227+
override val x: T
228+
) : ParametrizedSchema<T> {
229+
internal object CustomSerializer : KSerializer<ParametrizedContainer<Int>> by Companion.generatedSerializer(Int.serializer())
230+
}
231+
217232
fun box(): String = boxWrapper {
218233
val value = Value(42)
219234
val data = Data(42)
@@ -243,6 +258,10 @@ fun box(): String = boxWrapper {
243258
assertEquals("Container", Container.serializer().descriptor.serialName, "Container.serializer() illegal")
244259
assertEquals("""{"properties":[{"type":"Container","properties":[]}]}""", Json.encodeToString(Container.serializer(), Container(listOf(Container(emptyList())))))
245260
assertEquals("""{"properties":[{"type":"Container","properties":[]}]}""", Json.encodeToString(Container.CustomSerializer, Container(listOf(Container(emptyList())))))
261+
262+
assertEquals("ParametrizedContainer", ParametrizedContainer.serializer(Int.serializer()).descriptor.serialName, "Container.serializer() illegal")
263+
assertEquals("""{"properties":[{"type":"ParametrizedContainer","properties":[],"x":1}],"x":2}""", Json.encodeToString(ParametrizedContainer.serializer(Int.serializer()), ParametrizedContainer(listOf(ParametrizedContainer(emptyList(), 1)), 2)))
264+
assertEquals("""{"properties":[{"type":"ParametrizedContainer","properties":[],"x":1}],"x":2}""", Json.encodeToString(ParametrizedContainer.CustomSerializer, ParametrizedContainer(listOf(ParametrizedContainer(emptyList(), 1)), 2)))
246265
}
247266

248267
inline fun <reified T : Any> test(

‎plugins/kotlinx-serialization/testData/boxIr/generics.kt

+56
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,67 @@ data class Foo<T>(val i: Int, val t: T? = null)
99
@Serializable
1010
class Holder(val f1: Foo<String>, val f2: Foo<Int>)
1111

12+
13+
// tests for the issue from https://github.com/Kotlin/kotlinx.serialization/issues/2953#issuecomment-2735350353
14+
@Serializable
15+
sealed interface ParametrizedInterface<T>
16+
{
17+
val value: T
18+
}
19+
20+
@Serializable
21+
data class Value<T>(override val value: T): ParametrizedInterface<T>
22+
23+
@Serializable
24+
data class ParametrizedHolder<V>(val value: ParametrizedInterface<V>)
25+
26+
// mix type arguments, type parameter and simple type
27+
@Serializable
28+
sealed interface ParametrizedInterface2<T, K>
29+
{
30+
val value: T
31+
val value2: K
32+
}
33+
34+
@Serializable
35+
data class Value2<T, K>(override val value: T, override val value2: K): ParametrizedInterface2<T, K>
36+
37+
@Serializable
38+
data class ParametrizedHolder2<V>(val value: ParametrizedInterface2<V, Int>)
39+
40+
// all types are explicit
41+
@Serializable
42+
data class ParametrizedHolder0(val value1: ParametrizedInterface<Int>, val value2: ParametrizedInterface2<Int, Long>)
43+
44+
1245
fun box(): String {
1346
val holder = Holder(Foo(1, "1"), Foo(2))
1447
val str = Json.encodeToString(Holder.serializer(), holder)
1548
if (str != """{"f1":{"i":1,"t":"1"},"f2":{"i":2}}""") return str
1649
val decoded = Json.decodeFromString(Holder.serializer(), str)
1750
if (decoded.f1.t != holder.f1.t) return "f1.t: ${decoded.f1.t}"
51+
52+
assert("""{"value":{"type":"Value","value":1}}""") {
53+
Json.encodeToString(ParametrizedHolder(Value(1)))
54+
}?.let { return it }
55+
56+
assert("""{"value":{"type":"Value2","value":1,"value2":2}}""") {
57+
Json.encodeToString(ParametrizedHolder2(Value2(1, 2)))
58+
}?.let { return it }
59+
60+
assert("""{"value1":{"type":"Value","value":1},"value2":{"type":"Value2","value":1,"value2":2}}""") {
61+
Json.encodeToString(ParametrizedHolder0(Value(1), Value2(1, 2L)))
62+
}?.let { return it }
63+
1864
return "OK"
1965
}
66+
67+
private fun assert(expected: String, block: () -> String): String? {
68+
try {
69+
val actual = block()
70+
if (expected != actual) return actual
71+
return null
72+
} catch (e: Exception) {
73+
return e.message
74+
}
75+
}

0 commit comments

Comments
 (0)
Please sign in to comment.