kotlinx.serialization icon indicating copy to clipboard operation
kotlinx.serialization copied to clipboard

Internal compiler error: Stack overflow when trying to serialize a class with star projected recursive generic bound.

Open Sintrastes opened this issue 4 years ago • 10 comments

Describe the bug

I have a class using a self-recursive generic bound as follows, and elsewhere in my code I would like to serialize a usage of this class where the type parameter is projected. In the actual example in my code-base, the class with the self-recursive generic bound is actually a sealed class -- essentially an enum, actually, as all instances are objects, so it should be pretty easy to serialize. However, Kotlin seems to run into a stack overflow on such classes when the generic parameter is star projected.

To Reproduce Minimal non-working example:

@Serializable
class MinimalNonWorkingExample<X: MinimalNonWorkingExample<X>>

@Serializable
data class MinimalNonWorkingExamplePart2(
    val test: MinimalNonWorkingExample<*>
)

Trying to compile this leads the following stack trace:

e: java.lang.StackOverflowError
	at org.jetbrains.kotlin.types.TypeSubstitutor.substituteTypeArguments(TypeSubstitutor.java:365)
	at org.jetbrains.kotlin.types.TypeSubstitutor.substituteCompoundType(TypeSubstitutor.java:350)
	at org.jetbrains.kotlin.types.TypeSubstitutor.unsafeSubstitute(TypeSubstitutor.java:285)
	at org.jetbrains.kotlin.types.TypeSubstitutor.substituteTypeArguments(TypeSubstitutor.java:371)
	at org.jetbrains.kotlin.types.TypeSubstitutor.substituteCompoundType(TypeSubstitutor.java:350)
	at org.jetbrains.kotlin.types.TypeSubstitutor.unsafeSubstitute(TypeSubstitutor.java:285)
	at org.jetbrains.kotlin.types.TypeSubstitutor.substituteWithoutApproximation(TypeSubstitutor.java:162)
	at org.jetbrains.kotlin.types.TypeSubstitutor.substitute(TypeSubstitutor.java:147)
	at org.jetbrains.kotlin.types.TypeSubstitutor.substitute(TypeSubstitutor.java:140)
	at org.jetbrains.kotlin.types.TypeUtils.createSubstitutedSupertype(TypeUtils.java:263)
	at org.jetbrains.kotlin.types.TypeUtils.getImmediateSupertypes(TypeUtils.java:249)
	at org.jetbrains.kotlin.types.TypeUtils.collectAllSupertypes(TypeUtils.java:271)
	at org.jetbrains.kotlin.types.TypeUtils.getAllSupertypes(TypeUtils.java:284)
	at org.jetbrains.kotlin.types.typeUtil.TypeUtilsKt.supertypes(TypeUtils.kt:54)
	at org.jetbrains.kotlinx.serialization.compiler.diagnostic.SerializationPluginDeclarationChecker.checkSerializerNullability(SerializationPluginDeclarationChecker.kt:402)
	at org.jetbrains.kotlinx.serialization.compiler.diagnostic.SerializationPluginDeclarationChecker.checkType(SerializationPluginDeclarationChecker.kt:344)
	at org.jetbrains.kotlinx.serialization.compiler.diagnostic.SerializationPluginDeclarationChecker.checkTypeArguments(SerializationPluginDeclarationChecker.kt:307)
	at org.jetbrains.kotlinx.serialization.compiler.diagnostic.SerializationPluginDeclarationChecker.checkType(SerializationPluginDeclarationChecker.kt:345)
	at org.jetbrains.kotlinx.serialization.compiler.diagnostic.SerializationPluginDeclarationChecker.checkTypeArguments(SerializationPluginDeclarationChecker.kt:307)
	at org.jetbrains.kotlinx.serialization.compiler.diagnostic.SerializationPluginDeclarationChecker.checkType(SerializationPluginDeclarationChecker.kt:345)
	at org.jetbrains.kotlinx.serialization.compiler.diagnostic.SerializationPluginDeclarationChecker.checkTypeArguments(SerializationPluginDeclarationChecker.kt:307)
	at org.jetbrains.kotlinx.serialization.compiler.diagnostic.SerializationPluginDeclarationChecker.checkType(SerializationPluginDeclarationChecker.kt:345)
	at org.jetbrains.kotlinx.serialization.compiler.diagnostic.SerializationPluginDeclarationChecker.checkTypeArguments(SerializationPluginDeclarationChecker.kt:307)
        ... 

Expected behavior

The plugin should either generate a valid serializer, or at the very least warn me that this use case of generics is not supported -- possibly also directing me to an alternative way of defining a working serializer for the class.

Environment

  • Kotlin version: 1.5.31
  • Library version: kotlinx-serialization-json:1.3.0 (plugin version is the same as Kotlin version)
  • Kotlin platforms: JVM
  • Gradle version: 6.8

Note: Possibly related: https://github.com/Kotlin/kotlinx.serialization/issues/1501

Sintrastes avatar Oct 15 '21 16:10 Sintrastes

Even if this case would be detected, unfortunately serializers have parameters related to their type parameter. The type is actually recursive so infinite serializers need to be created. The only way to solve this is to allow an annotation on a type parameter to mark it as ignored for serialization (or to detect unused parameters and omit their code for generated serializers). Then the plugin could detect the issue and provide a suggestion for a fix.

pdvrieze avatar Oct 15 '21 18:10 pdvrieze

Is there any workaround for this with the current version of kotlinx.serialization?

ov7a avatar Nov 28 '22 12:11 ov7a

I'm not sure that kotlinx.serialization is supposed to work with recursive types at all

sandwwraith avatar Nov 29 '22 15:11 sandwwraith

@sandwwraith that's up to being designed (by default, I guess it's ok not to support them), but plugin definitely not supposed to crash with StackoverflowError

qwwdfsad avatar Nov 30 '22 11:11 qwwdfsad

just hit the issue, would be nice to get it fixed at some point in future

ninja- avatar May 25 '23 01:05 ninja-

I think I just stumbled over this issue when trying to write a serializer for javax.measure.Unit. The original definition for that one is interface Unit<Q extends Quantity<Q>>. But the serialization doesn't even need the type parameter to work.

My initial serializer implementation looked like this:

class UnitSerializer(): KSerializer<javax.measure.Unit<*>> {
    override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("MeasureUnit", PrimitiveKind.STRING)
    override fun deserialize(decoder: Decoder): javax.measure.Unit<*> {
        val string = decoder.decodeString()
        return SimpleUnitFormat.getInstance().parse(string)
    }

    override fun serialize(encoder: Encoder, value: javax.measure.Unit<*>) {
        val string = SimpleUnitFormat.getInstance().format(value)
        encoder.encodeString(string)
    }
}

But when trying to compile my code, I get an infinite loop / Stack Overflow error:

	at org.jetbrains.kotlin.types.KotlinType.getAnnotations(KotlinType.kt:57)
	at org.jetbrains.kotlinx.serialization.compiler.resolve.KSerializationUtilKt.overriddenSerializer(KSerializationUtil.kt:253)
	at org.jetbrains.kotlinx.serialization.compiler.backend.common.TypeUtilKt.findTypeSerializer(TypeUtil.kt:150)
	at org.jetbrains.kotlinx.serialization.compiler.backend.common.TypeUtilKt.findTypeSerializerOrContextUnchecked(TypeUtil.kt:132)
	at org.jetbrains.kotlinx.serialization.compiler.diagnostic.SerializationPluginDeclarationChecker.checkType(SerializationPluginDeclarationChecker.kt:508)
	at org.jetbrains.kotlinx.serialization.compiler.diagnostic.SerializationPluginDeclarationChecker.checkTypeArguments(SerializationPluginDeclarationChecker.kt:473)
	at org.jetbrains.kotlinx.serialization.compiler.diagnostic.SerializationPluginDeclarationChecker.checkType(SerializationPluginDeclarationChecker.kt:513)
	at org.jetbrains.kotlinx.serialization.compiler.diagnostic.SerializationPluginDeclarationChecker.checkTypeArguments(SerializationPluginDeclarationChecker.kt:473)
	at org.jetbrains.kotlinx.serialization.compiler.diagnostic.SerializationPluginDeclarationChecker.checkType(SerializationPluginDeclarationChecker.kt:513)
...

milgner avatar Aug 29 '23 09:08 milgner

@milgner In your specific case you might be able to avoid it using KSerializer<javax.measure.Unit<Nothing>>. Using Nothing should fit in the type constraints while also being a non-related serializer. Note that erasure comes into play here, so if you use the serializer for a generic type parameter it doesn't "officially" match, that will not cause a compilation error - but all the usual unchecked cast warnings apply (even if the IDE/compiler doesn't give them)

pdvrieze avatar Aug 29 '23 10:08 pdvrieze

@milgner In your specific case you might be able to avoid it using KSerializer<javax.measure.Unit<Nothing>>. Using Nothing should fit in the type constraints while also being a non-related serializer. Note that erasure comes into play here, so if you use the serializer for a generic type parameter it doesn't "officially" match, that will not cause a compilation error - but all the usual unchecked cast warnings apply (even if the IDE/compiler doesn't give them)

Nice! It does indeed work if I change the type of the property from javax.measure.Unit<*> to javax.measure.Unit<Nothing>. Unfortunately that makes using that property very cumbersome as I now have to cast every time when instantiating or reading that value... :grimacing: I could probably work around that by adding a secondary constructor and attribute reader - but it gets quite hacky at that point... :dizzy:

milgner avatar Aug 29 '23 11:08 milgner

@milgner What you do is that you basically use either a typealias, or some other little helper functions that do the mapping/casting for you (rather than javax.measure.Unit.serializer() you use javax.measure.Unit.unitSerializer()- or whatever name you prefer. Note that it should work in any case when you use the annotations@Serializable(with=MyUnitSerializer::class)` (for which typealias works well) as that is erased.

pdvrieze avatar Aug 29 '23 14:08 pdvrieze

I came across this error too today. I invented a workaround using polymorphic serialization since my architecture already uses it a lot. I created a custom serializer that uses an utility private surrogate class. In my code, SingleVersion is a subclass of Version. Pay attention at the differences of ReleasedService and SurrogateService implementation and how the custom serializer tries to solve the problem:

@Serializable(with = ReleasedService.Companion.ServiceSerializer::class)
internal data class ReleasedService(
    override val name: String,
    override val version: SingleVersion<*>,
) : Service<Version> {

    companion object {

        object ServiceSerializer : KSerializer<ReleasedService> {
            override val descriptor: SerialDescriptor
                get() = SurrogateService.serializer().descriptor

            override fun deserialize(decoder: Decoder): ReleasedService {
                val sur = decoder.decodeSerializableValue(SurrogateService.serializer())
                require(sur.version is SingleVersion<*>) {
                    "The version of a Service must be a SingleVersion"
                }
                return ReleasedService(
                    sur.name,
                    sur.version,
                )
            }

            override fun serialize(encoder: Encoder, value: ReleasedService) {
                encoder.encodeSerializableValue(
                    SurrogateService.serializer(),
                    SurrogateService(
                        value.name,
                        value.version,
                    ),
                )
            }

            @Serializable
            @SerialName("Service")
            private data class SurrogateService(
                val name: String,
                val version: Version
            ) {
                init {
                    require(version is SingleVersion<*>) {
                        "The version of a Service must be a SingleVersion"
                    }
                }
            }
        }
    }
}

Note that to make this work properly it is needed to configure the polymorphic block of the formatter in the correct way. All the documentation can be found in the polymorphism guide. This solution is "ok" for my architecture, but it may be "not that good" for others. Please before using it be sure to know what's going on with typings.

AngeloFilaseta avatar Oct 02 '23 15:10 AngeloFilaseta