ANR/Deadlock during Json initialization with a custom serializer
Describe the bug When initializing a Json instance with a SerializersModule that includes a custom KSerializer, a class initialization deadlock can occur, leading to an ANR (Application Not Responding) on Android. This seems to happen specifically when the custom serializer's descriptor is initialized eagerly (without by lazy).
To Reproduce Create a custom KSerializer for any type (e.g., java.time.Instant). Initialize its descriptor property directly.
object InstantSerializer : KSerializer<Instant> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("Instant", PrimitiveKind.STRING)
// ...
}
Create a singleton object that configures a Json instance using this serializer.
object JsonHolder {
private val serializersModule: SerializersModule = SerializersModule {
contextual(Instant::class, InstantSerializer)
}
val baseJson: Json by lazy {
Json {
serializersModule = JsonHolder.serializersModule
explicitNulls = false
ignoreUnknownKeys = true
encodeDefaults = true
coerceInputValues = true
}
}
}
Access the JsonHolder singleton for the first time on the main thread during application startup, for example, as part of a Dagger dependency graph initialization.
main (runnable):tid=[TID] systid=[SYS_TID]
#00 pc [ADDRESS] libart.so (art::DumpNativeStack + [OFFSET]) (BuildId: [BUILD_ID])
#01 pc [ADDRESS] libart.so (art::Thread::DumpStack const + [OFFSET]) (BuildId: [BUILD_ID])
#02 pc [ADDRESS] libart.so (art::DumpCheckpoint::Run + [OFFSET]) (BuildId: [BUILD_ID])
#03 pc [ADDRESS] libart.so (art::Thread::RunCheckpointFunction + [OFFSET]) (BuildId: [BUILD_ID])
#04 pc [ADDRESS] libart.so (artTestSuspendFromCode + [OFFSET]) (BuildId: [BUILD_ID])
#05 pc [ADDRESS] libart.so (art_quick_test_suspend + [OFFSET]) (BuildId: [BUILD_ID])
at java.lang.String.fillBytesLatin1(Native method)
at java.lang.String.fillBytes(String.java:4400)
at java.lang.AbstractStringBuilder.putStringAt(AbstractStringBuilder.java:1693)
at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:549)
at java.lang.StringBuilder.append(StringBuilder.java:186)
at kotlinx.serialization.internal.PrimitiveArrayDescriptor.<init>(CollectionDescriptors.kt:114)
at kotlinx.serialization.internal.PrimitiveArraySerializer.<init>(CollectionSerializers.kt:147)
at kotlinx.serialization.internal.BooleanArraySerializer.<init>(PrimitiveArraysSerializers.kt:370)
at kotlinx.serialization.internal.BooleanArraySerializer.<clinit>(PrimitiveArraysSerializers.kt:12)
at kotlinx.serialization.builtins.BuiltinSerializersKt.BooleanArraySerializer(BuiltinSerializers.kt:174)
at kotlinx.serialization.internal.PlatformKt.initBuiltins(Platform.kt:188)
at kotlinx.serialization.internal.PrimitivesKt.<clinit>(Primitives.kt:18)
at kotlinx.serialization.descriptors.SerialDescriptorsKt.PrimitiveSerialDescriptor(SerialDescriptors.kt:91)
at com.mycompany.app.core.data.json.InstantSerializer.<clinit>(InstantSerializer.kt:18)
at com.mycompany.app.core.data.utils.JsonProvider.<clinit>(JsonProvider.kt:13)
at com.mycompany.app.core.data.di.module.CoreDataModule$JsonInner.provideJson(CoreDataModule.java:94)
at com.mycompany.app.core.data.di.module.CoreDataModule_JsonInner_ProvideJsonFactory.provideJson(CoreDataModule_JsonInner_ProvideJsonFactory.java:38)
at com.mycompany.app.core.data.di.DaggerCoreDataComponent$CoreDataComponentImpl.getBaseJson(DaggerCoreDataComponent.java:168)
at com.mycompany.app.core.network.di.DaggerCoreNetworkComponent$CoreNetworkComponentImpl.getBaseJsonRetrofitConverterFactory(DaggerCoreNetworkComponent.java:441)
at com.mycompany.app.feature.featureA.impl.di.DaggerRetrofitBinderComponent$RetrofitBinderComponentImpl.getRetrofit(DaggerRetrofitBinderComponent.java:55)
at com.mycompany.app.feature.featureA.impl.di.DaggerExperimentsFeatureComponent$ExperimentsFeatureComponentImpl$SwitchingProvider.get(DaggerExperimentsFeatureComponent.java:145)
at dagger.internal.DoubleCheck.getSynchronized(DoubleCheck.java:54)
at dagger.internal.DoubleCheck.get(DoubleCheck.java:45)
at com.mycompany.app.feature.featureA.impl.di.DaggerExperimentsFeatureComponent$ExperimentsFeatureComponentImpl$SwitchingProvider.get(DaggerExperimentsFeatureComponent.java:142)
at dagger.internal.DoubleCheck.getSynchronized(DoubleCheck.java:54)
at dagger.internal.DoubleCheck.get(DoubleCheck.java:45)
at com.mycompany.app.feature.featureA.impl.di.DaggerExperimentsFeatureComponent$ExperimentsFeatureComponentImpl$SwitchingProvider.get(DaggerExperimentsFeatureComponent.java:139)
at dagger.internal.DoubleCheck.getSynchronized(DoubleCheck.java:54)
at dagger.internal.DoubleCheck.get(DoubleCheck.java:45)
at com.mycompany.app.feature.featureA.impl.di.DaggerExperimentsFeatureComponent$ExperimentsFeatureComponentImpl.getGenericExperimentsInteractor(DaggerExperimentsFeatureComponent.java:111)
at com.mycompany.app.feature.featureB.impl.di.DaggerFeatureToggleApiComponent$FeatureToggleApiComponentImpl$SwitchingProvider.get(DaggerFeatureToggleApiComponent.java:209)
at dagger.internal.DoubleCheck.getSynchronized(DoubleCheck.java:54)
at dagger.internal.DoubleCheck.get(DoubleCheck.java:45)
Expected behavior
The Json object should be created and initialized without freezing the main thread or causing an ANR. The application should start up smoothly.
Environment
- Kotlin version: 2.1.21
- Library version: 1.7.3
- Platforms: Android
- Gradle version: 8.14
How does it work if you don't use lazy? Note that the object will already synchronize initialization and only be loaded when needed (close to where you need serialization).
Looking at your stacktrace there is a circular dependency where InstantSerializer depends on the file primitives.kt, so I suspect that you want to make sure not to introduce unintended dependencies in the underlying class loaders. (The issue for the hang is not visible in the code you provided)
Updated stacktrace