Serializing sub-objects raises NPE
Describe the bug Trying to serialize an object inside another object raises a NullPointerException:
Exception in thread "main" java.lang.NullPointerException
at kotlinx.serialization.encoding.Encoder$DefaultImpls.encodeNullableSerializableValue(Encoding.kt:268)
at kotlinx.serialization.json.JsonEncoder$DefaultImpls.encodeNullableSerializableValue(JsonEncoder.kt)
at kotlinx.serialization.json.internal.StreamingJsonEncoder.encodeNullableSerializableValue(StreamingJsonEncoder.kt:15)
at kotlinx.serialization.encoding.AbstractEncoder.encodeNullableSerializableElement(AbstractEncoder.kt:81)
at com.mayak.discord.rest.settings.MessageCreateSettings.write$Self(Settings.kt:13)
at com.mayak.discord.rest.settings.MessageCreateSettings$$serializer.serialize(Settings.kt)
at com.mayak.discord.rest.settings.MessageCreateSettings$$serializer.serialize(Settings.kt:10)
at kotlinx.serialization.json.internal.StreamingJsonEncoder.encodeSerializableValue(StreamingJsonEncoder.kt:223)
at kotlinx.serialization.json.Json.encodeToString(Json.kt:73)
at org.example.MainKt$main$1.invokeSuspend(Main.kt:25)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
at kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.common.kt:274)
at kotlinx.coroutines.BlockingCoroutine.joinBlocking(Builders.kt:86)
at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking(Builders.kt:61)
at kotlinx.coroutines.BuildersKt.runBlocking(Unknown Source)
at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking$default(Builders.kt)
at kotlinx.coroutines.BuildersKt.runBlocking$default(Unknown Source)
at org.example.MainKt.main(Main.kt:11)
at org.example.MainKt.main(Main.kt)
To Reproduce Attach a code snippet or test data if possible.
// the code i used for testing
fun main() {
val embed = Embed().apply {
title = "a"
}
val settings = MessageCreateSettings(embed = embed)
println(Json.encodeToString(settings))
}
@Serializable
sealed class Settings
@Serializable
class MessageCreateSettings(
val content: String? = null,
val tts: Boolean = false,
val embed: Embed? = null // error points to this line
): Settings()
@Serializable(with = EmbedSerializer::class)
class Embed private constructor(
var title: String? = null,
var description: String? = null,
@SerialName("fields") internal val _fields: MutableList<EmbedField> = mutableListOf(),
internal val type: String = "rich"
) {
constructor(): this(null)
@Serializable
data class EmbedField(val name: String, val value: String, val inline: Boolean)
val fields: List<EmbedField>
get() = _fields
fun addField(name: String, value: String, inline: Boolean): Embed {
_fields.add(EmbedField(name, value, inline))
return this
}
fun addField(name: String, value: String) = addField(name, value, false)
}
object EmbedSerializer: KSerializer<Embed> {
override val descriptor = Embed.serializer().descriptor
private val mapSerializer = MapSerializer(String.serializer(), JsonElement.serializer())
override fun deserialize(decoder: Decoder): Embed {
val map = decoder.decodeSerializableValue(mapSerializer)
return Embed().apply {
map["title"]?.let { title = Json.decodeFromJsonElement(String.serializer(), it) }
map["description"]?.let { description = Json.decodeFromJsonElement(String.serializer(), it) }
map["fields"]?.jsonArray?.map { f ->
Json.decodeFromJsonElement(Embed.EmbedField.serializer(), f)
}?.forEach(_fields::add)
}
}
override fun serialize(encoder: Encoder, value: Embed) {
val map = mutableMapOf<String, JsonElement>()
map["type"] = Json.encodeToJsonElement(String.serializer(), value.type)
value.title?.let { map["title"] = Json.encodeToJsonElement(String.serializer(), it) }
value.description?.let { map["description"] = Json.encodeToJsonElement(String.serializer(), it) }
if (value.fields.isNotEmpty())
map["fields"] = Json.encodeToJsonElement(value.fields)
encoder.encodeSerializableValue(mapSerializer, map)
}
}
Expected behavior For it to correctly encode the two objects, the embed inside the settings.
Environment
- Kotlin version: 1.4.21
- Library version: 1.0.1
- Kotlin platforms: JVM
- Gradle version: 6.7
- IDE version (if bug is related to the IDE) N/A
- Other relevant context Windows 10, JRE 1.8.0_252
I got a similar case, not sure if it's related.
@Serializable
abstract class BaseType {
@Serializable
object TypeA : BaseType() { }
@Serializable
object TypeB : BaseType() { }
companion object {
val serializerModule = SerializersModule {
polymorphic(BaseType::class) {
subclass(TypeA::class)
subclass(TypeB::class)
}
}
}
}
With this I get java.lang.ExceptionInInitializerError during tests. The stackrace includes:
Caused by: java.lang.NullPointerException: Cannot invoke "foo.bar.BaseType$TypeA.serializer()" because "foo.bar.BaseType$TypeA.INSTANCE" is null
at foo.bar.BaseType.<clinit>(BaseType.kt:0)
Worked around it by wrapping the SerializersModule in lazy initialization:
companion object {
val serializerModule by lazy {
SerializersModule {
polymorphic(BaseType::class) {
subclass(TypeA::class)
subclass(TypeB::class)
}
}
}
}
@XuaTheGrate Your problem is that EmbedSerializer is the serializer for Embed, but defines its descriptor to be that of Embed's serializer (this happens to be the same serializer). As such there is no actual descriptor. If you want to use the default serializer, add @KeepGeneratedSerializer and then access it through Embed.generatedSerializer() and you will actually have a descriptor to reuse.
@brnhffmnn Your problem is just an initialisation loop where the classes have circular dependencies. The could might be generated to not have this loop, but in some cases it is not avoidable (I haven't checked the specific code generated). Using lazy is one way to resolve it.