Skip to content

Problem with deserializing singleton object in polymorphic context with custom serializer #2830

Open
@nsk90

Description

@nsk90

Describe the bug
I have a Singleton object, for which I am trying to implement a KSerializer.
Serializer works fine only if it is used outside of polymorphic context.
When it is used in polymorphic context deserialization fails with unexpected exception:

 * Unexpected JSON token at offset 9: Expected end of the object '}', but had '"' instead at path: $.type
 * JSON input: {"type":{"type":"Singleton"}}
 * kotlinx.serialization.json.internal.JsonDecodingException: Unexpected JSON token at offset 9: Expected end of the object '}', but had '"' instead at path: $.type
 * JSON input: {"type":{"type":"Singleton"}}

1)Am I doing something wrong, or it is the library issue?
2) looks that I have to use api marked as internal (not working also) - buildSerialDescriptor("Singleton", StructureKind.OBJECT)

To Reproduce
Attach a code snippet or test data if possible.
I have provided 4 tests.
1 and 2 looks approximately like I expect, but they fail, 3 and 4 are workarounds that I have tried.

package com.nsk.test

import io.kotest.core.spec.style.StringSpec
import kotlinx.serialization.InternalSerializationApi
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.builtins.nullable
import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.descriptors.*
import kotlinx.serialization.encodeToString
import kotlinx.serialization.encoding.*
import kotlinx.serialization.json.Json
import kotlinx.serialization.modules.SerializersModule
import kotlinx.serialization.modules.polymorphic

/** Requires custom serializer */
private interface PolymorphicType

/** Requires custom serializer */
private object Singleton : PolymorphicType

@Serializable
private data class MyData(val type: PolymorphicType)

/**
 * Does not work.
 * Uses [buildClassSerialDescriptor]
 */
private object NotWorkingSingletonSerializer : KSerializer<Singleton> {
    override val descriptor = buildClassSerialDescriptor("Singleton")

    override fun serialize(encoder: Encoder, value: Singleton) {
        encoder.encodeStructure(descriptor) {}
    }

    override fun deserialize(decoder: Decoder): Singleton {
        return decoder.decodeStructure(descriptor) { Singleton }
    }
}

/**
 * Does not work.
 * This looks like correct variant for me, but is uses internal api with [StructureKind.OBJECT]
 */
private object NotWorkingObjectSingletonSerializer : KSerializer<Singleton> {
    @OptIn(InternalSerializationApi::class)
    // have to use internal api
    override val descriptor = buildSerialDescriptor("Singleton", StructureKind.OBJECT)

    override fun serialize(encoder: Encoder, value: Singleton) {
        encoder.encodeStructure(descriptor) {}
    }

    override fun deserialize(decoder: Decoder): Singleton {
        return decoder.decodeStructure(descriptor) { Singleton }
    }
}

/**
 * Works fine
 * But I have to add fake field
 */
private object WorkingSerializableSerializer : KSerializer<Singleton> {
    override val descriptor = buildClassSerialDescriptor("Singleton") {
        element<Boolean>("field_to_fix_an_issue")
    }

    override fun serialize(encoder: Encoder, value: Singleton) {
        encoder.encodeStructure(descriptor) {
            encodeBooleanElement(descriptor, 0, false)
        }
    }

    override fun deserialize(decoder: Decoder): Singleton {
        return decoder.decodeStructure(descriptor) {
            while (true) {
                when (val index = decodeElementIndex(descriptor)) {
                    0 -> decodeBooleanElement(descriptor, 0) // just read it
                    CompositeDecoder.DECODE_DONE -> break
                    else -> error("Unexpected index: $index")
                }
            }
            Singleton
        }
    }
}

/**
 * Works fine, allows to omit "field_to_fix_an_issue" in json, but it is still in code.
 */
private object WorkingOptionalSerializableSerializer : KSerializer<Singleton> {
    override val descriptor = buildClassSerialDescriptor("Singleton") {
        element("field_to_fix_an_issue", Boolean.serializer().nullable.descriptor, isOptional = true)
    }

    override fun serialize(encoder: Encoder, value: Singleton) {
        encoder.encodeStructure(descriptor) {
            encodeNullableSerializableElement(descriptor, 0, Boolean.serializer().nullable, null)
        }
    }

    override fun deserialize(decoder: Decoder): Singleton {
        return decoder.decodeStructure(descriptor) {
            while (true) {
                when (val index = decodeElementIndex(descriptor)) {
                    0 -> decodeNullableSerializableElement(descriptor, 0, Boolean.serializer().nullable) // just read it
                    CompositeDecoder.DECODE_DONE -> break
                    else -> error("Unexpected index: $index")
                }
            }
            Singleton
        }
    }
}

class ObjectSerializationTest : StringSpec({
    "test1 NotWorkingSingletonSerializer - fail" {
        val jsonFormat = Json {
            serializersModule = SerializersModule {
                polymorphic(PolymorphicType::class) {
                    subclass(Singleton::class, NotWorkingSingletonSerializer)
                }
            }
        }
        val json = jsonFormat.encodeToString(MyData(Singleton))
        println(json) // {"type":{"type":"Singleton"}}
        val data = jsonFormat.decodeFromString<MyData>(json)
        /*
         * deserialization exception:
         * Unexpected JSON token at offset 9: Expected end of the object '}', but had '"' instead at path: $.type
         * JSON input: {"type":{"type":"Singleton"}}
         * kotlinx.serialization.json.internal.JsonDecodingException: Unexpected JSON token at offset 9: Expected end of the object '}', but had '"' instead at path: $.type
         * JSON input: {"type":{"type":"Singleton"}}
         */
    }

    "test2 NotWorkingObjectSingletonSerializer - fail" {
        val jsonFormat = Json {
            serializersModule = SerializersModule {
                polymorphic(PolymorphicType::class) {
                    subclass(Singleton::class, NotWorkingObjectSingletonSerializer)
                }
            }
        }
        val json = jsonFormat.encodeToString(MyData(Singleton))
        println(json) // {"type":{"type":"Singleton"}}
        val data = jsonFormat.decodeFromString<MyData>(json)
        /*
         * deserialization exception:
         * Unexpected JSON token at offset 9: Expected end of the object '}', but had '"' instead at path: $.type
         * JSON input: {"type":{"type":"Singleton"}}
         * kotlinx.serialization.json.internal.JsonDecodingException: Unexpected JSON token at offset 9: Expected end of the object '}', but had '"' instead at path: $.type
         * JSON input: {"type":{"type":"Singleton"}}
         */
    }

    "test3 WorkingSerializableSerializer - ok" {
        val jsonFormat = Json {
            serializersModule = SerializersModule {
                polymorphic(PolymorphicType::class) {
                    subclass(Singleton::class, WorkingSerializableSerializer)
                }
            }
        }
        val json = jsonFormat.encodeToString(MyData(Singleton))
        println(json) // {"type":{"type":"Singleton","field_to_fix_an_issue":false}}
        val data = jsonFormat.decodeFromString<MyData>(json)
    }

    "test4 WorkingOptionalSerializableSerializer - ok" {
        val jsonFormat = Json {
            serializersModule = SerializersModule {
                polymorphic(PolymorphicType::class) {
                    subclass(Singleton::class, WorkingOptionalSerializableSerializer)
                }
            }
        }
        val json = jsonFormat.encodeToString(MyData(Singleton))
        println(json) // {"type":{"type":"Singleton","field_to_fix_an_issue":false}} or {"type":{"type":"Singleton"}}
        val data = jsonFormat.decodeFromString<MyData>(json)
    }
})

Expected behavior

All 4 cases pass successfully

Environment

  • Kotlin version: [2.0.20]
  • Library version: [1.7.3]
  • Kotlin platforms: [JVM]
  • Gradle version: [7.6.1]
  • IDE version -

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions