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

Serializing sub-objects raises NPE

Open XuaTheGrate opened this issue 4 years ago • 2 comments

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

XuaTheGrate avatar Jan 20 '21 03:01 XuaTheGrate