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

Decoding polymorphic class from keyword other than "type"

Open daniel-jasinski opened this issue 5 years ago • 8 comments

What is your use-case and why do you need this feature?

I am writing a serializer for a custom dictionary-like format that may require multiple polymorphic types to read from a dictionary. I need a data like this:

@Serializable
data class Container(
    @InlineStruct
    val type1: Type1s,
    @InlineStruct
    val type2: Type2s
)

/**
* Puts the content of the class on the same
* level as the content of the parent class.
*/
@SerialInfo
annotation class InlineStruct

@Serializable
sealed class Type1s

@Serializable
@SerialName("example")
data class Example1(
    val val1: Int
) : Type1s()



@Serializable
sealed class Type2s

@Serializable
@SerialName("example")
data class Example2(
    val val2: Int
) : Type2s()

to be serialized/deserialized from a dictionary like this:

type1     example;
val1      42;
type2     example;
val2      13;

Inlining the classes (@InlineStruct) is simple, however deserializing polymorphic type with key other than "type" requires hacking into some internal API:

// need to check if this is a Polymorphic serializer
if(deserializer is SealedClassSerializer<*>) {
     // decode the actual type name from custom key
     val klassName = decodeStringElement(descriptor, index)
     // find the actual serializer for this type
     val serializer = deserializer.findPolymorphicSerializer(this, klassName)
     // decode the actual type normally
    return serializer.deserialize(decoder) as T
}

Describe the solution you'd like

The issue described here is due to the specific nature of this format. Making the required internal API "less internal" or provide a public equivalent of this API would be enough to handle this issue.

daniel-jasinski avatar Nov 25 '20 11:11 daniel-jasinski

So the problem is SealedClassSerializer and findPolymorphicSerializer being internal? For the sealed, you can replace it with the check of the kind. For the latter, indeed, it seems that function is crucial for the authors of custom formats

sandwwraith avatar Dec 11 '20 16:12 sandwwraith

Using SealedClassSerializer and findPolymorphicSerializer is a workaround for the main issue of "type" being a hardcoded keyword for discovering the polymorphic type. In my example, we have two different types that have to be polymorphically resolved (keywords "type1" and "type2").

daniel-jasinski avatar Dec 26 '20 12:12 daniel-jasinski

The 'type' keyword is purely a JSON setting and should not affect other formats. In binary formats, for example, an old-fashioned array polymorphism is used

sandwwraith avatar Dec 28 '20 10:12 sandwwraith

I'm writing a custom binary format and have come across an issue very similar to this.

I don't really need findPolymorphicSerializer because I can inspect the SerialDescriptor for every possible element. (I'm only handling cases where all subclasses are known at compile time).

The issue is, there are other ways to have PolymorphicKind.SEALED descriptors. The SealedClassSerializer has this.

override val descriptor: SerialDescriptor = buildSerialDescriptor(serialName, PolymorphicKind.SEALED) {
	element("type", String.serializer().descriptor)
	val elementDescriptor =
		buildSerialDescriptor("kotlinx.serialization.Sealed<${baseClass.simpleName}>", SerialKind.CONTEXTUAL) {
			subclassSerializers.forEach {
				val d = it.descriptor
				element(d.serialName, d)
			}
		}
	element("value", elementDescriptor)
}

but I can imagine someone writing a custom sealed serializer for Either<Int, String, ....> like this.

override val descriptor: SerialDescriptor = buildSerialDescriptor(serialName, PolymorphicKind.SEALED) {
	subclassSerializers.forEach {
		val d = it.descriptor
		element(d.serialName, d)
	}
}

I think the array (or default) polymorphism should be pushed down to the decoder/encoder level instead of the serializer level (Like is more/less done for nullability). In fact, I'd go as far to say that the AbstractPolymorphicSerializer descriptors are not really describing themselves properly.

Once I finish the decoder for sealed classes, I'll update the issue with blockers/pain points.

Dominaezzz avatar May 24 '21 19:05 Dominaezzz

After some thought, I think what I want is some standard way to express sealed descriptors. That is well documented or not internal. Either will do.

Dominaezzz avatar May 25 '21 19:05 Dominaezzz

The format of serializers is documented and stable. A serial descriptor for sealed class is required to enumerate all the subclass serializers for it. The defaults do this, and formats do/should rely on it (or use the extension method SerializersModule.getPolymorhpicDescriptors. Of course this is fragile when developers don't implement it correctly, one of the reasons that buildSerialDescriptor is locked behind an OptIn. The name however is not relevant. In those cases where you need/want to know the base class, you need to use SerialDescriptor.capturedKClass. The reason that it uses contextual is to allow formats to use shared code paths.

pdvrieze avatar May 26 '21 10:05 pdvrieze

SerializersModule.getPolymorhpicDescriptors doesn't work for sealed classes I think. But it does make sense for the descriptors to be stable.

So I was writing a CompositeDecoder to make the SealedSerializer work and it's a little awkward. I thought it would make sense to earlier intercept decodeSerializableValue and check if deserializer is SealedClassSerializer<*> but now I need access to the serializer of the subclasses to make this work. There's the findPolymorphicSerializerOrNull method but it requires a CompositeDecoder which I don't have in the Encoder sadly. So now I'm back to the awkward CompositeDecoder solution.

Dominaezzz avatar May 31 '21 14:05 Dominaezzz

Related: #1715

sandwwraith avatar Dec 23 '24 11:12 sandwwraith