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

Support using an integer as the class/case discriminator in polymorphic serialization #2587

Open
wants to merge 12 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
39 changes: 36 additions & 3 deletions core/api/kotlinx-serialization-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ public final class kotlinx/serialization/PolymorphicSerializer : kotlinx/seriali
public final class kotlinx/serialization/PolymorphicSerializerKt {
public static final fun findPolymorphicSerializer (Lkotlinx/serialization/internal/AbstractPolymorphicSerializer;Lkotlinx/serialization/encoding/CompositeDecoder;Ljava/lang/String;)Lkotlinx/serialization/DeserializationStrategy;
public static final fun findPolymorphicSerializer (Lkotlinx/serialization/internal/AbstractPolymorphicSerializer;Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)Lkotlinx/serialization/SerializationStrategy;
public static final fun findPolymorphicSerializerWithNumber (Lkotlinx/serialization/internal/AbstractPolymorphicSerializer;Lkotlinx/serialization/encoding/CompositeDecoder;Ljava/lang/Integer;)Lkotlinx/serialization/DeserializationStrategy;
}

public abstract interface annotation class kotlinx/serialization/Required : java/lang/annotation/Annotation {
Expand All @@ -79,6 +80,7 @@ public final class kotlinx/serialization/SealedClassSerializer : kotlinx/seriali
public fun <init> (Ljava/lang/String;Lkotlin/reflect/KClass;[Lkotlin/reflect/KClass;[Lkotlinx/serialization/KSerializer;[Ljava/lang/annotation/Annotation;)V
public fun findPolymorphicSerializerOrNull (Lkotlinx/serialization/encoding/CompositeDecoder;Ljava/lang/String;)Lkotlinx/serialization/DeserializationStrategy;
public fun findPolymorphicSerializerOrNull (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)Lkotlinx/serialization/SerializationStrategy;
public fun findPolymorphicSerializerWithNumberOrNull (Lkotlinx/serialization/encoding/CompositeDecoder;Ljava/lang/Integer;)Lkotlinx/serialization/DeserializationStrategy;
public fun getBaseClass ()Lkotlin/reflect/KClass;
public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
}
Expand All @@ -99,6 +101,21 @@ public abstract interface annotation class kotlinx/serialization/SerialName : ja
public abstract fun value ()Ljava/lang/String;
}

public abstract interface annotation class kotlinx/serialization/SerialPolymorphicNumber : java/lang/annotation/Annotation {
public abstract fun baseClass ()Ljava/lang/Class;
public abstract fun number ()I
}

public abstract interface annotation class kotlinx/serialization/SerialPolymorphicNumber$Container : java/lang/annotation/Annotation {
public abstract fun value ()[Lkotlinx/serialization/SerialPolymorphicNumber;
}

public synthetic class kotlinx/serialization/SerialPolymorphicNumber$Impl : kotlinx/serialization/SerialPolymorphicNumber {
public fun <init> (Lkotlin/reflect/KClass;I)V
public final synthetic fun baseClass ()Ljava/lang/Class;
public final synthetic fun number ()I
}

public abstract interface annotation class kotlinx/serialization/Serializable : java/lang/annotation/Annotation {
public abstract fun with ()Ljava/lang/Class;
}
Expand Down Expand Up @@ -153,6 +170,13 @@ public abstract interface annotation class kotlinx/serialization/UseContextualSe
public abstract fun forClasses ()[Ljava/lang/Class;
}

public abstract interface annotation class kotlinx/serialization/UseSerialPolymorphicNumbers : java/lang/annotation/Annotation {
}

public synthetic class kotlinx/serialization/UseSerialPolymorphicNumbers$Impl : kotlinx/serialization/UseSerialPolymorphicNumbers {
public fun <init> ()V
}

public abstract interface annotation class kotlinx/serialization/UseSerializers : java/lang/annotation/Annotation {
public abstract fun serializerClasses ()[Ljava/lang/Class;
}
Expand Down Expand Up @@ -280,13 +304,19 @@ public abstract interface class kotlinx/serialization/descriptors/SerialDescript
public abstract fun getElementsCount ()I
public abstract fun getKind ()Lkotlinx/serialization/descriptors/SerialKind;
public abstract fun getSerialName ()Ljava/lang/String;
public abstract fun getSerialPolymorphicNumberByBaseClass ()Ljava/util/Map;
public abstract fun getSerialPolymorphicNumberByBaseClass (Lkotlin/reflect/KClass;)I
public abstract fun getUseSerialPolymorphicNumbers ()Z
public abstract fun isElementOptional (I)Z
public abstract fun isInline ()Z
public abstract fun isNullable ()Z
}

public final class kotlinx/serialization/descriptors/SerialDescriptor$DefaultImpls {
public static fun getAnnotations (Lkotlinx/serialization/descriptors/SerialDescriptor;)Ljava/util/List;
public static fun getSerialPolymorphicNumberByBaseClass (Lkotlinx/serialization/descriptors/SerialDescriptor;)Ljava/util/Map;
public static fun getSerialPolymorphicNumberByBaseClass (Lkotlinx/serialization/descriptors/SerialDescriptor;Lkotlin/reflect/KClass;)I
public static fun getUseSerialPolymorphicNumbers (Lkotlinx/serialization/descriptors/SerialDescriptor;)Z
public static fun isInline (Lkotlinx/serialization/descriptors/SerialDescriptor;)Z
public static fun isNullable (Lkotlinx/serialization/descriptors/SerialDescriptor;)Z
}
Expand Down Expand Up @@ -561,6 +591,7 @@ public abstract class kotlinx/serialization/internal/AbstractPolymorphicSerializ
public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
public fun findPolymorphicSerializerOrNull (Lkotlinx/serialization/encoding/CompositeDecoder;Ljava/lang/String;)Lkotlinx/serialization/DeserializationStrategy;
public fun findPolymorphicSerializerOrNull (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)Lkotlinx/serialization/SerializationStrategy;
public fun findPolymorphicSerializerWithNumberOrNull (Lkotlinx/serialization/encoding/CompositeDecoder;Ljava/lang/Integer;)Lkotlinx/serialization/DeserializationStrategy;
public abstract fun getBaseClass ()Lkotlin/reflect/KClass;
public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
}
Expand Down Expand Up @@ -958,7 +989,7 @@ public final class kotlinx/serialization/internal/PluginExceptionsKt {
public static final fun throwMissingFieldException (IILkotlinx/serialization/descriptors/SerialDescriptor;)V
}

public class kotlinx/serialization/internal/PluginGeneratedSerialDescriptor : kotlinx/serialization/descriptors/SerialDescriptor, kotlinx/serialization/internal/CachedNames {
public class kotlinx/serialization/internal/PluginGeneratedSerialDescriptor : kotlinx/serialization/internal/CachedNames {
public fun <init> (Ljava/lang/String;Lkotlinx/serialization/internal/GeneratedSerializer;I)V
public synthetic fun <init> (Ljava/lang/String;Lkotlinx/serialization/internal/GeneratedSerializer;IILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun addElement (Ljava/lang/String;Z)V
Expand All @@ -975,8 +1006,6 @@ public class kotlinx/serialization/internal/PluginGeneratedSerialDescriptor : ko
public fun getSerialNames ()Ljava/util/Set;
public fun hashCode ()I
public fun isElementOptional (I)Z
public fun isInline ()Z
public fun isNullable ()Z
public final fun pushAnnotation (Ljava/lang/annotation/Annotation;)V
public final fun pushClassAnnotation (Ljava/lang/annotation/Annotation;)V
public fun toString ()Ljava/lang/String;
Expand Down Expand Up @@ -1286,6 +1315,7 @@ public final class kotlinx/serialization/modules/PolymorphicModuleBuilder {
public final fun buildTo (Lkotlinx/serialization/modules/SerializersModuleBuilder;)V
public final fun default (Lkotlin/jvm/functions/Function1;)V
public final fun defaultDeserializer (Lkotlin/jvm/functions/Function1;)V
public final fun defaultDeserializerForNumber (Lkotlin/jvm/functions/Function1;)V
public final fun subclass (Lkotlin/reflect/KClass;Lkotlinx/serialization/KSerializer;)V
}

Expand All @@ -1296,6 +1326,7 @@ public abstract class kotlinx/serialization/modules/SerializersModule {
public static synthetic fun getContextual$default (Lkotlinx/serialization/modules/SerializersModule;Lkotlin/reflect/KClass;Ljava/util/List;ILjava/lang/Object;)Lkotlinx/serialization/KSerializer;
public abstract fun getPolymorphic (Lkotlin/reflect/KClass;Ljava/lang/Object;)Lkotlinx/serialization/SerializationStrategy;
public abstract fun getPolymorphic (Lkotlin/reflect/KClass;Ljava/lang/String;)Lkotlinx/serialization/DeserializationStrategy;
public abstract fun getPolymorphicWithNumber (Lkotlin/reflect/KClass;Ljava/lang/Integer;)Lkotlinx/serialization/DeserializationStrategy;
}

public final class kotlinx/serialization/modules/SerializersModuleBuilder : kotlinx/serialization/modules/SerializersModuleCollector {
Expand All @@ -1307,6 +1338,7 @@ public final class kotlinx/serialization/modules/SerializersModuleBuilder : kotl
public fun polymorphic (Lkotlin/reflect/KClass;Lkotlin/reflect/KClass;Lkotlinx/serialization/KSerializer;)V
public fun polymorphicDefault (Lkotlin/reflect/KClass;Lkotlin/jvm/functions/Function1;)V
public fun polymorphicDefaultDeserializer (Lkotlin/reflect/KClass;Lkotlin/jvm/functions/Function1;)V
public fun polymorphicDefaultDeserializerForNumber (Lkotlin/reflect/KClass;Lkotlin/jvm/functions/Function1;)V
public fun polymorphicDefaultSerializer (Lkotlin/reflect/KClass;Lkotlin/jvm/functions/Function1;)V
}

Expand All @@ -1324,6 +1356,7 @@ public abstract interface class kotlinx/serialization/modules/SerializersModuleC
public abstract fun polymorphic (Lkotlin/reflect/KClass;Lkotlin/reflect/KClass;Lkotlinx/serialization/KSerializer;)V
public abstract fun polymorphicDefault (Lkotlin/reflect/KClass;Lkotlin/jvm/functions/Function1;)V
public abstract fun polymorphicDefaultDeserializer (Lkotlin/reflect/KClass;Lkotlin/jvm/functions/Function1;)V
public abstract fun polymorphicDefaultDeserializerForNumber (Lkotlin/reflect/KClass;Lkotlin/jvm/functions/Function1;)V
public abstract fun polymorphicDefaultSerializer (Lkotlin/reflect/KClass;Lkotlin/jvm/functions/Function1;)V
}

Expand Down
22 changes: 22 additions & 0 deletions core/commonMain/src/kotlinx/serialization/Annotations.kt
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,28 @@ public annotation class Serializer(
// @Retention(AnnotationRetention.RUNTIME) still runtime, but KT-41082
public annotation class SerialName(val value: String)

/**
* Requires all subclasses marked with this annotation to use [SerialPolymorphicNumber].
*/
@SerialInfo
@Target(AnnotationTarget.CLASS)
@ExperimentalSerializationApi
public annotation class UseSerialPolymorphicNumbers

/**
* When its parent class is annotated with [UseSerialPolymorphicNumbers],
* overrides its [String]-typed serial name when serialized as a subclass of the parent class in [baseClass]
* (including the value overridden by [SerialName] if set)
* with a [Int]-typed number in [number].
*
* Using a number instead of a string shortens the size of the serialized message, especially in a binary format.
*/
@SerialInfo
@Target(AnnotationTarget.CLASS)
@Repeatable
@ExperimentalSerializationApi
public annotation class SerialPolymorphicNumber(val baseClass: KClass<*>, val number: Int)

/**
* Indicates that property must be present during deserialization process, despite having a default value.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,13 @@ public fun <T : Any> AbstractPolymorphicSerializer<T>.findPolymorphicSerializer(
): DeserializationStrategy<T> =
findPolymorphicSerializerOrNull(decoder, klassName) ?: throwSubtypeNotRegistered(klassName, baseClass)

@InternalSerializationApi
public fun <T : Any> AbstractPolymorphicSerializer<T>.findPolymorphicSerializerWithNumber(
decoder: CompositeDecoder,
serialPolymorphicNumber: Int?
): DeserializationStrategy<T> =
findPolymorphicSerializerWithNumberOrNull(decoder, serialPolymorphicNumber) ?: throwSubtypeNotRegistered(serialPolymorphicNumber, baseClass)

@InternalSerializationApi
public fun <T : Any> AbstractPolymorphicSerializer<T>.findPolymorphicSerializer(
encoder: Encoder,
Expand Down
26 changes: 26 additions & 0 deletions core/commonMain/src/kotlinx/serialization/SealedSerializer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -140,13 +140,39 @@ public class SealedClassSerializer<T : Any>(
}.mapValues { it.value.value }
}

private val serialPolymorphicNumber2Serializer: Map<Int, KSerializer<out T>>? by lazy(LazyThreadSafetyMode.PUBLICATION) {
if (descriptor.useSerialPolymorphicNumbers)
class2Serializer.entries.groupingBy {
it.value.descriptor.getSerialPolymorphicNumberByBaseClass(baseClass)
}
.aggregate<Map.Entry<KClass<out T>, KSerializer<out T>>, Int, Map.Entry<KClass<*>, KSerializer<out T>>>
{ key, accumulator, element, _ ->
if (accumulator != null) {
error(
"Multiple sealed subclasses of '$baseClass' have the same serial polymorphic number '$key':" +
" '${accumulator.key}', '${element.key}'"
)
}
element
}.mapValues { it.value.value }
else
null
}

override fun findPolymorphicSerializerOrNull(
decoder: CompositeDecoder,
klassName: String?
): DeserializationStrategy<T>? {
return serialName2Serializer[klassName] ?: super.findPolymorphicSerializerOrNull(decoder, klassName)
}

@InternalSerializationApi
override fun findPolymorphicSerializerWithNumberOrNull(
decoder: CompositeDecoder, serialPolymorphicNumber: Int?
): DeserializationStrategy<T>? =
serialPolymorphicNumber2Serializer!![serialPolymorphicNumber]
?: super.findPolymorphicSerializerWithNumberOrNull(decoder, serialPolymorphicNumber)

override fun findPolymorphicSerializerOrNull(encoder: Encoder, value: T): SerializationStrategy<T>? {
return (class2Serializer[value::class] ?: super.findPolymorphicSerializerOrNull(encoder, value))?.cast()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ package kotlinx.serialization.descriptors
import kotlinx.serialization.*
import kotlinx.serialization.builtins.*
import kotlinx.serialization.encoding.*
import kotlin.reflect.*

/**
* Serial descriptor is an inherent property of [KSerializer] that describes the structure of the serializable type.
Expand Down Expand Up @@ -203,6 +204,36 @@ public interface SerialDescriptor {
@ExperimentalSerializationApi
public val annotations: List<Annotation> get() = emptyList()

/**
* TODO
*/
@ExperimentalSerializationApi
public val useSerialPolymorphicNumbers: Boolean
get() =
annotations.any { it is UseSerialPolymorphicNumbers }

/**
* TODO
*/
@ExperimentalSerializationApi
public val serialPolymorphicNumberByBaseClass: Map<KClass<*>, Int>
get() =
annotations.asSequence().mapNotNull { it as? SerialPolymorphicNumber }
.groupBy { it.baseClass }
.mapValues {
it.value.singleOrNull()?.number
?: throw SerializationException("duplicate base classes in `@SerialPolymorphicNumber` annotations registered for $serialName")
}

@ExperimentalSerializationApi
public fun getSerialPolymorphicNumberByBaseClass(baseClass: KClass<*>): Int =
serialPolymorphicNumberByBaseClass.getOrElse(baseClass) {
throw SerializationException(
"The serial polymorphic number for `$serialName` in the scope of `${baseClass.simpleName}` is not found. " +
"Please annotate the class with `@SerialPolymorphicNumber` with the first argument being `${baseClass.simpleName}`."
)
}

/**
* Returns a positional name of the child at the given [index].
* Positional name represents a corresponding property name in the class, associated with
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@ import kotlin.reflect.*
* }
* ```
*/
@Suppress("FunctionName")
@OptIn(ExperimentalSerializationApi::class)
public fun buildClassSerialDescriptor(
serialName: String,
Expand Down Expand Up @@ -310,7 +309,7 @@ internal class SerialDescriptorImpl(
override val elementsCount: Int,
typeParameters: List<SerialDescriptor>,
builder: ClassSerialDescriptorBuilder
) : SerialDescriptor, CachedNames {
) : CommonSerialDescriptor(), CachedNames {

override val annotations: List<Annotation> = builder.annotations
override val serialNames: Set<String> = builder.elementNames.toHashSet()
Expand Down