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

ANR/Deadlock during Json initialization with a custom serializer

Open VEZE opened this issue 4 months ago • 2 comments

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

VEZE avatar Aug 22 '25 15:08 VEZE

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)

pdvrieze avatar Aug 23 '25 16:08 pdvrieze

Updated stacktrace

VEZE avatar Aug 24 '25 19:08 VEZE