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

Wrapping SealedClassSerializer yields wrong serialization results #2838

Closed
valeriyo opened this issue Oct 20, 2024 · 5 comments
Closed

Wrapping SealedClassSerializer yields wrong serialization results #2838

valeriyo opened this issue Oct 20, 2024 · 5 comments
Labels

Comments

@valeriyo
Copy link

With serialization 1.7.3:

@Serializable
sealed interface Sealed {

  @Serializable
  @SerialName("SealedDataClass")
  data object SealedDataClass : Sealed
}

Serializing SealedDataClass returns {"type":"SealedDataClass"} as expected:

    val serializer = Sealed.serializer()
    val encoded = encodeToString(serializer, Sealed.SealedDataClass)
    assertEquals("{\"type\":\"SealedDataClass\"}", encoded)

However, if I simply wrap the default serializer like this:

  class Wrapped<T : Any>(
    private val sealedSerializer: KSerializer<T>,
  ) : KSerializer<T> {

    override val descriptor: SerialDescriptor
      get() = sealedSerializer.descriptor

    override fun deserialize(decoder: Decoder): T =
      sealedSerializer.deserialize(decoder)

    override fun serialize(encoder: Encoder, value: T) {
      sealedSerializer.serialize(encoder, value)
    }
  }

The result is completely different (and wrong) -- ["SealedDataClass",{}]:

    val serializer2 = Wrapped(Sealed.serializer())
    val encoded2 = encodeToString(serializer2, Sealed.SealedDataClass)
    assertEquals("[\"SealedDataClass\",{}]", encoded2)

This is puzzling, because with the default serializer, debugger (Android Studio Koala) skips over encodeToString even if I try to step in -- but with a wrapped serializer, I can actually debug by stepping through normally.

What's going on?

P.S. I'm trying to wrap SealedClassSerializer so I can handle deserialization exceptions better.

@valeriyo
Copy link
Author

The "wrong" result ["SealedDataClass",{}] is produced by AbstractPolymorphicSerializer:

    public final override fun serialize(encoder: Encoder, value: T) {
        val actualSerializer = findPolymorphicSerializer(encoder, value)
        encoder.encodeStructure(descriptor) {
            encodeStringElement(descriptor, 0, actualSerializer.descriptor.serialName)
            encodeSerializableElement(descriptor, 1, actualSerializer.cast(), value)
        }
    }

but why....?

@valeriyo
Copy link
Author

It's probably checks like these (below) that make this impossible to wrap SealedClassSerializer (derived from AbstractPolymorphicSerializer) - where AbstractPolymorphicSerializer is abstract class with internal constructor:

@Suppress("UNCHECKED_CAST")
internal inline fun <T> JsonEncoder.encodePolymorphically(
    serializer: SerializationStrategy<T>,
    value: T,
    ifPolymorphic: (String) -> Unit
) {
    if (serializer !is AbstractPolymorphicSerializer<*> || json.configuration.useArrayPolymorphism) {
        serializer.serialize(this, value)
        return
    }
    val casted = serializer as AbstractPolymorphicSerializer<Any>
    val baseClassDiscriminator = serializer.descriptor.classDiscriminator(json)
    val actualSerializer = casted.findPolymorphicSerializer(this, value as Any)
    validateIfSealed(casted, actualSerializer, baseClassDiscriminator)
    checkKind(actualSerializer.descriptor.kind)
    ifPolymorphic(baseClassDiscriminator)
    actualSerializer.serialize(this, value)
}

@valeriyo
Copy link
Author

Also, opened #2839 -- most likely that issue is also due to JsonEncoder taking bad shortcuts

@pdvrieze
Copy link
Contributor

One of the issues at the least is that the descriptors need to have a context (through calling withContext - which is internal). If you want to do your own wrapper, the best choice is to make your serializer Contextual as that tells the format that it cannot assume the description of the actual value (contextual will be resolved to an actual serializer "later")

@sandwwraith
Copy link
Member

Try rewriting your wrapper as

override fun deserialize(decoder: Decoder): T = decoder.decodeSerializableValue(sealedSerializer)

override fun serialize(encoder: Encoder, value: T) {
    encoder.encodeSerializableValue(sealedSerializer, value)
}

encodeSerializableValue will pass the correct serializer to JsonEncoder.encodePolymorphically. By calling serialize(decoder) directly, you're skipping this step and getting incorrect result

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

3 participants