Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Can't register both a default serializer and deserializer #1837

Closed
aSemy opened this issue Jan 22, 2022 · 10 comments
Closed

Can't register both a default serializer and deserializer #1837

aSemy opened this issue Jan 22, 2022 · 10 comments
Assignees
Labels

Comments

@aSemy
Copy link
Contributor

aSemy commented Jan 22, 2022

I want to register a polymorphic KSerializer in a SerializersModule. When encoding/decoding with JSON, I want to discriminate differently to when I am encoding in another format.

(I am trying to find a work around for #1664 - the non-accessible default 'type' field disrupts other systems.)

I've tried various combinations of registering serializers - with an annotation, in the serializer module using polymorphic or contextual - but it seems this isn't possible.

val myProjectJsonSerializersModule = SerializersModule {
  polymorphicDefaultDeserializer(Packet::class) { PacketJsonSerializer }
  polymorphicDefaultSerializer(Packet::class) { PacketJsonSerializer }
}

// java.lang.IllegalArgumentException: Default serializers provider for 
// class class Packet is already registered: 
// (kotlin.String?) -> kotlinx.serialization.DeserializationStrategy<out Packet>?

(aside: it looks like there's a typo in the error message, 'class' appears twice: "class class Packet")

Expected behavior

Encoding/decoding with discriminators should be format-independent, and should not require hidden/non-accessible type fields.

Environment

  • Kotlin version: 1.6.10
  • Library version: 1.3.2
  • Kotlin platforms: all
  • Gradle version: 7.3.3
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
import kotlinx.serialization.DeserializationStrategy
import kotlinx.serialization.EncodeDefault
import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonContentPolymorphicSerializer
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.contentOrNull
import kotlinx.serialization.json.encodeToJsonElement
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import kotlinx.serialization.modules.SerializersModule


@Serializable
sealed class Packet {
  @EncodeDefault
  abstract val packetType: Type

  enum class Type {
    SOLID,
    LIQUID,
    GAS,
  }
}

@Serializable
data class GasPacket(
  val isToxic: Boolean,
) : Packet() {
  @EncodeDefault
  override val packetType: Type = Type.GAS
}


@Serializable
data class LiquidPacket(
  val colour: String,
) : Packet() {
  @EncodeDefault
  override val packetType: Type = Type.LIQUID
}


@Serializable
data class SolidPacket(
  val density: Long,
) : Packet() {
  @EncodeDefault
  override val packetType: Type = Type.SOLID
}


object PacketJsonSerializer : JsonContentPolymorphicSerializer<Packet>(
  Packet::class
) {
  private val key = Packet::packetType.name

  override fun selectDeserializer(element: JsonElement): DeserializationStrategy<out Packet> {

    val type: Packet.Type? = element
      .jsonObject[key]
      ?.jsonPrimitive
      ?.contentOrNull
      ?.let { json ->
        Packet.Type.values().firstOrNull { it.name == json }
      }

    requireNotNull(type) { "Unknown Packet.Type ${key}: $element" }

    return when (type) {
      Packet.Type.SOLID  -> SolidPacket.serializer()
      Packet.Type.LIQUID -> LiquidPacket.serializer()
      Packet.Type.GAS    -> GasPacket.serializer()
    }
  }
}

val jsonPacketMapper = Json {
  prettyPrint = true
  prettyPrintIndent = "  "

serializersModule = SerializersModule {
  polymorphicDefaultDeserializer(Packet::class) { PacketJsonSerializer }
  polymorphicDefaultSerializer(Packet::class) { PacketJsonSerializer }
}
}

class TestDefaultSerDes {

  private val gasPacket: Packet = GasPacket(
    true
  )

  private val gasPacketJsonString =
    """
      {
        "isToxic": true,
        "packetType": "GAS"
      }
    """.trimIndent()

  @Test
  fun expectEncodedJsonElementHasNoTypeDiscriminator() {
    val encodedJsonElementGasPacket = jsonPacketMapper.encodeToJsonElement(gasPacket)

    val expectedJsonElement = buildJsonObject {
      put("isToxic", JsonPrimitive(true))
      put("packetType", JsonPrimitive("GAS"))
    }

    assertEquals(expectedJsonElement, encodedJsonElementGasPacket)
  }

  @Test
  fun expectJsonStringIsDecodedToPacket() {
    val packet: Packet = jsonPacketMapper.decodeFromString(gasPacketJsonString)
    assertEquals(Packet.Type.GAS, packet.packetType)
  }

  @Test
  fun expectJsonStringIsDecodedToGasPacket() {
    val gas: GasPacket = jsonPacketMapper.decodeFromString(gasPacketJsonString)
    assertTrue(gas.isToxic)
    assertEquals(Packet.Type.GAS, gas.packetType)
  }

}
@Dominaezzz
Copy link
Contributor

I don't see why you need to use polymorphicDefaultSerializer and polymorphicDefaultDeserializer here.
You've already circumvented the library's built-in polymorphic serialisation by using a custom serializer.

You can just do this.

@Serializable(PacketJsonSerializer::class)
sealed class Packet

@aSemy
Copy link
Contributor Author

aSemy commented Feb 2, 2022

@Dominaezzz how would that work if I'd like to serialise/deserialise Packet to/from a non-json format? Doesn't annotating Packet with PacketJsonSerializer mean I can only use json?

@Dominaezzz
Copy link
Contributor

Oh, I missed that requirement.
So you're planning on writing a serialiser for every format then?
In which case you can use contextual serialisation instead.

@aSemy
Copy link
Contributor Author

aSemy commented Feb 2, 2022

I haven't got that far yet, because of the bug :)

I think I tried contextual serialization, but it didn't work because that is only for determining fields. It didn't work for Packet.

@pdvrieze
Copy link
Contributor

pdvrieze commented Feb 3, 2022

@aSemy The way I would do it is is the way I have some special types/serializers for the XML context (like serializing document fragments directly). Basically it is perfectly valid to check the type of the encoder/decoder in the encode and decode functions and handle the json coders specially. Just do the "normal" thing for other formats, unless you want to special case those too. Note that this is special casing with a normal fallback so shouldn't really have a problem with using runtime types.

From my perspective this is the only really sane way to do it in that it doesn't put any burden upon the users of the types/serializers or upon formats/the serialization library.

@aSemy
Copy link
Contributor Author

aSemy commented Feb 4, 2022

@pdvrieze I don't understand what you're trying to explain. I'm not using XML. I need to use a SerializersModule to define format-specific serialization/deserialization strategies so the DTOs aren't restricted to a specific format, but there's a bug. It looks like the cause has been identified and fixed in #1849.

@pdvrieze
Copy link
Contributor

pdvrieze commented Feb 4, 2022

@aSemy The mention of XML is just as an example. What you want to do is something like (just using Long as example):

object MyCustomSerializer: KSerializer<Long> {
   override val descriptor: SerialDescriptor = TODO()

   override fun deserialize(decoder: Decoder): Long = when(decoder) {
     is JsonDecoder -> decoder.decodeString().toLong()
     else -> decoder.decodeLong()
   }

   override fun serializer(encoder: Encoder, value: Long): Unit = when (encoder) {
     is JsonEncoder -> encoder.encodeString(value.toString())
     else -> encoder.encodeLong(value)
   }
}

pdvrieze pushed a commit to pdvrieze/kotlinx.serialization that referenced this issue Apr 29, 2022
@aSemy
Copy link
Contributor Author

aSemy commented May 7, 2022

Hi @sandwwraith, thanks for fixing it. I'd like to use it, is there a plan for releasing a new version? Or is there a nightly version I can use?

@Omara8
Copy link

Omara8 commented Jul 29, 2022

how can i use the merged solutions?

@sandwwraith
Copy link
Member

Fix is available in the new 1.4.0-RC version

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

5 participants