diff --git a/core/api/kotlinx-serialization-core.api b/core/api/kotlinx-serialization-core.api index 4b46dcca17..8d22cb4fe3 100644 --- a/core/api/kotlinx-serialization-core.api +++ b/core/api/kotlinx-serialization-core.api @@ -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 { @@ -79,6 +80,7 @@ public final class kotlinx/serialization/SealedClassSerializer : kotlinx/seriali public fun (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; } @@ -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 (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; } @@ -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 ()V +} + public abstract interface annotation class kotlinx/serialization/UseSerializers : java/lang/annotation/Annotation { public abstract fun serializerClasses ()[Ljava/lang/Class; } @@ -280,6 +304,9 @@ 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 @@ -287,6 +314,9 @@ public abstract interface class kotlinx/serialization/descriptors/SerialDescript 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 } @@ -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 } @@ -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 (Ljava/lang/String;Lkotlinx/serialization/internal/GeneratedSerializer;I)V public synthetic fun (Ljava/lang/String;Lkotlinx/serialization/internal/GeneratedSerializer;IILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun addElement (Ljava/lang/String;Z)V @@ -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; @@ -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 } @@ -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 { @@ -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 } @@ -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 } diff --git a/core/commonMain/src/kotlinx/serialization/Annotations.kt b/core/commonMain/src/kotlinx/serialization/Annotations.kt index 67104dc3c6..cf99594f11 100644 --- a/core/commonMain/src/kotlinx/serialization/Annotations.kt +++ b/core/commonMain/src/kotlinx/serialization/Annotations.kt @@ -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. */ diff --git a/core/commonMain/src/kotlinx/serialization/PolymorphicSerializer.kt b/core/commonMain/src/kotlinx/serialization/PolymorphicSerializer.kt index 6ee7071735..95fd2c98f9 100644 --- a/core/commonMain/src/kotlinx/serialization/PolymorphicSerializer.kt +++ b/core/commonMain/src/kotlinx/serialization/PolymorphicSerializer.kt @@ -101,6 +101,13 @@ public fun AbstractPolymorphicSerializer.findPolymorphicSerializer( ): DeserializationStrategy = findPolymorphicSerializerOrNull(decoder, klassName) ?: throwSubtypeNotRegistered(klassName, baseClass) +@InternalSerializationApi +public fun AbstractPolymorphicSerializer.findPolymorphicSerializerWithNumber( + decoder: CompositeDecoder, + serialPolymorphicNumber: Int? +): DeserializationStrategy = + findPolymorphicSerializerWithNumberOrNull(decoder, serialPolymorphicNumber) ?: throwSubtypeNotRegistered(serialPolymorphicNumber, baseClass) + @InternalSerializationApi public fun AbstractPolymorphicSerializer.findPolymorphicSerializer( encoder: Encoder, diff --git a/core/commonMain/src/kotlinx/serialization/SealedSerializer.kt b/core/commonMain/src/kotlinx/serialization/SealedSerializer.kt index 52b7c0544d..ccb14c1dba 100644 --- a/core/commonMain/src/kotlinx/serialization/SealedSerializer.kt +++ b/core/commonMain/src/kotlinx/serialization/SealedSerializer.kt @@ -140,6 +140,25 @@ public class SealedClassSerializer( }.mapValues { it.value.value } } + private val serialPolymorphicNumber2Serializer: Map>? by lazy(LazyThreadSafetyMode.PUBLICATION) { + if (descriptor.useSerialPolymorphicNumbers) + class2Serializer.entries.groupingBy { + it.value.descriptor.getSerialPolymorphicNumberByBaseClass(baseClass) + } + .aggregate, KSerializer>, Int, Map.Entry, KSerializer>> + { 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? @@ -147,6 +166,13 @@ public class SealedClassSerializer( return serialName2Serializer[klassName] ?: super.findPolymorphicSerializerOrNull(decoder, klassName) } + @InternalSerializationApi + override fun findPolymorphicSerializerWithNumberOrNull( + decoder: CompositeDecoder, serialPolymorphicNumber: Int? + ): DeserializationStrategy? = + serialPolymorphicNumber2Serializer!![serialPolymorphicNumber] + ?: super.findPolymorphicSerializerWithNumberOrNull(decoder, serialPolymorphicNumber) + override fun findPolymorphicSerializerOrNull(encoder: Encoder, value: T): SerializationStrategy? { return (class2Serializer[value::class] ?: super.findPolymorphicSerializerOrNull(encoder, value))?.cast() } diff --git a/core/commonMain/src/kotlinx/serialization/descriptors/SerialDescriptor.kt b/core/commonMain/src/kotlinx/serialization/descriptors/SerialDescriptor.kt index 17fdbfe0f7..b735a6219a 100644 --- a/core/commonMain/src/kotlinx/serialization/descriptors/SerialDescriptor.kt +++ b/core/commonMain/src/kotlinx/serialization/descriptors/SerialDescriptor.kt @@ -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. @@ -203,6 +204,36 @@ public interface SerialDescriptor { @ExperimentalSerializationApi public val annotations: List get() = emptyList() + /** + * TODO + */ + @ExperimentalSerializationApi + public val useSerialPolymorphicNumbers: Boolean + get() = + annotations.any { it is UseSerialPolymorphicNumbers } + + /** + * TODO + */ + @ExperimentalSerializationApi + public val serialPolymorphicNumberByBaseClass: Map, 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 diff --git a/core/commonMain/src/kotlinx/serialization/descriptors/SerialDescriptors.kt b/core/commonMain/src/kotlinx/serialization/descriptors/SerialDescriptors.kt index cb380aafc0..6fe2e39a8e 100644 --- a/core/commonMain/src/kotlinx/serialization/descriptors/SerialDescriptors.kt +++ b/core/commonMain/src/kotlinx/serialization/descriptors/SerialDescriptors.kt @@ -49,7 +49,6 @@ import kotlin.reflect.* * } * ``` */ -@Suppress("FunctionName") @OptIn(ExperimentalSerializationApi::class) public fun buildClassSerialDescriptor( serialName: String, @@ -310,7 +309,7 @@ internal class SerialDescriptorImpl( override val elementsCount: Int, typeParameters: List, builder: ClassSerialDescriptorBuilder -) : SerialDescriptor, CachedNames { +) : CommonSerialDescriptor(), CachedNames { override val annotations: List = builder.annotations override val serialNames: Set = builder.elementNames.toHashSet() diff --git a/core/commonMain/src/kotlinx/serialization/internal/AbstractPolymorphicSerializer.kt b/core/commonMain/src/kotlinx/serialization/internal/AbstractPolymorphicSerializer.kt index 26d3b5e27f..bb7f0b2f1f 100644 --- a/core/commonMain/src/kotlinx/serialization/internal/AbstractPolymorphicSerializer.kt +++ b/core/commonMain/src/kotlinx/serialization/internal/AbstractPolymorphicSerializer.kt @@ -7,6 +7,7 @@ package kotlinx.serialization.internal import kotlinx.serialization.* import kotlinx.serialization.encoding.* import kotlin.jvm.* +import kotlin.properties.* import kotlin.reflect.* /** @@ -31,13 +32,19 @@ public abstract class AbstractPolymorphicSerializer internal constructo public final override fun serialize(encoder: Encoder, value: T) { val actualSerializer = findPolymorphicSerializer(encoder, value) encoder.encodeStructure(descriptor) { - encodeStringElement(descriptor, 0, actualSerializer.descriptor.serialName) + if (descriptor.useSerialPolymorphicNumbers) + encodeIntElement( + descriptor, 0, actualSerializer.descriptor.getSerialPolymorphicNumberByBaseClass(baseClass) + ) + else + encodeStringElement(descriptor, 0, actualSerializer.descriptor.serialName) encodeSerializableElement(descriptor, 1, actualSerializer.cast(), value) } } public final override fun deserialize(decoder: Decoder): T = decoder.decodeStructure(descriptor) { var klassName: String? = null + var serialPolymorphicNumber: Int? = null var value: Any? = null if (decodeSequentially()) { return@decodeStructure decodeSequentially(this) @@ -48,14 +55,25 @@ public abstract class AbstractPolymorphicSerializer internal constructo CompositeDecoder.DECODE_DONE -> { break@mainLoop } + 0 -> { - klassName = decodeStringElement(descriptor, index) + if (descriptor.useSerialPolymorphicNumbers) + serialPolymorphicNumber = decodeIntElement(descriptor, index) + else + klassName = decodeStringElement(descriptor, index) } + 1 -> { - klassName = requireNotNull(klassName) { "Cannot read polymorphic value before its type token" } - val serializer = findPolymorphicSerializer(this, klassName) + val serializer = if (descriptor.useSerialPolymorphicNumbers) { + requireNotNull(serialPolymorphicNumber) { "Cannot read polymorphic value before its type token" } + findPolymorphicSerializerWithNumber(this, serialPolymorphicNumber) + } else { + requireNotNull(klassName) { "Cannot read polymorphic value before its type token" } + findPolymorphicSerializer(this, klassName) + } value = decodeSerializableElement(descriptor, index, serializer) } + else -> throw SerializationException( "Invalid index in polymorphic deserialization of " + (klassName ?: "unknown class") + @@ -83,6 +101,16 @@ public abstract class AbstractPolymorphicSerializer internal constructo klassName: String? ): DeserializationStrategy? = decoder.serializersModule.getPolymorphic(baseClass, klassName) + /** + * TODO + */ + @InternalSerializationApi + public open fun findPolymorphicSerializerWithNumberOrNull( + decoder: CompositeDecoder, + serialPolymorphicNumber: Int? + ): DeserializationStrategy? = + decoder.serializersModule.getPolymorphicWithNumber(baseClass, serialPolymorphicNumber) + /** * Lookups an actual serializer for given [value] within the current [base class][baseClass]. @@ -109,6 +137,22 @@ internal fun throwSubtypeNotRegistered(subClassName: String?, baseClass: KClass< ) } +@JvmName("throwSubtypeNotRegistered") +internal fun throwSubtypeNotRegistered(serialPolymorphicNumber: Int?, baseClass: KClass<*>): Nothing { + val scope = "in the polymorphic scope of '${baseClass.simpleName}'" + throw SerializationException( + ( + if (serialPolymorphicNumber == null) + "Class discriminator (serial polymorphic number) was missing and no default serializers were registered $scope." + else + "Serializer for subclass serial polymorphic number '$serialPolymorphicNumber' is not found in $scope.\n" + + "Check if class with serial polymorphic number '$serialPolymorphicNumber' exists and serializer is registered in a corresponding SerializersModule.\n" + + "To be registered automatically, class annotated with '@SerialPolymorphicNumber($serialPolymorphicNumber)' has to be '@Serializable', and the base class '${baseClass.simpleName}' marked with `@UseSerialPolymorphicNumbers` has to be sealed and '@Serializable'.\n" + ) + + "\nRemove the `@UseSerialPolymorphicNumbers` annotation from the base class `${baseClass.simpleName}` if you want to switch back to polymorphic serialization using the serial name strings." + ) +} + @JvmName("throwSubtypeNotRegistered") internal fun throwSubtypeNotRegistered(subClass: KClass<*>, baseClass: KClass<*>): Nothing = throwSubtypeNotRegistered(subClass.simpleName ?: "$subClass", baseClass) diff --git a/core/commonMain/src/kotlinx/serialization/internal/CommonSerialDescriptor.kt b/core/commonMain/src/kotlinx/serialization/internal/CommonSerialDescriptor.kt new file mode 100644 index 0000000000..3834d585b1 --- /dev/null +++ b/core/commonMain/src/kotlinx/serialization/internal/CommonSerialDescriptor.kt @@ -0,0 +1,17 @@ +/* + * Copyright 2017-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.serialization.internal + +import kotlinx.serialization.* +import kotlinx.serialization.descriptors.* +import kotlin.reflect.* + +internal abstract class CommonSerialDescriptor : SerialDescriptor { + @ExperimentalSerializationApi + override val useSerialPolymorphicNumbers: Boolean by lazy { super.useSerialPolymorphicNumbers } + + @ExperimentalSerializationApi + override val serialPolymorphicNumberByBaseClass: Map, Int> by lazy { super.serialPolymorphicNumberByBaseClass } +} \ No newline at end of file diff --git a/core/commonMain/src/kotlinx/serialization/internal/PluginGeneratedSerialDescriptor.kt b/core/commonMain/src/kotlinx/serialization/internal/PluginGeneratedSerialDescriptor.kt index a954bdab00..0053097907 100644 --- a/core/commonMain/src/kotlinx/serialization/internal/PluginGeneratedSerialDescriptor.kt +++ b/core/commonMain/src/kotlinx/serialization/internal/PluginGeneratedSerialDescriptor.kt @@ -18,7 +18,7 @@ internal open class PluginGeneratedSerialDescriptor( override val serialName: String, private val generatedSerializer: GeneratedSerializer<*>? = null, final override val elementsCount: Int -) : SerialDescriptor, CachedNames { +) : CommonSerialDescriptor(), CachedNames { override val kind: SerialKind get() = StructureKind.CLASS override val annotations: List get() = classAnnotations ?: emptyList() diff --git a/core/commonMain/src/kotlinx/serialization/modules/PolymorphicModuleBuilder.kt b/core/commonMain/src/kotlinx/serialization/modules/PolymorphicModuleBuilder.kt index 1b8d431e1a..798842f152 100644 --- a/core/commonMain/src/kotlinx/serialization/modules/PolymorphicModuleBuilder.kt +++ b/core/commonMain/src/kotlinx/serialization/modules/PolymorphicModuleBuilder.kt @@ -21,7 +21,16 @@ public class PolymorphicModuleBuilder @PublishedApi internal cons ) { private val subclasses: MutableList, KSerializer>> = mutableListOf() private var defaultSerializerProvider: ((Base) -> SerializationStrategy?)? = null - private var defaultDeserializerProvider: ((String?) -> DeserializationStrategy?)? = null + private var defaultDeserializerProvider: PolymorphicDeserializerProvider? = null + private var defaultDeserializerProviderForNumber: PolymorphicDeserializerProviderForNumber? = null + + /* + // TODO implement this or remove? + /** + * If specified, overrides [SerializersModuleBuilder.allUseSerialPolymorphicNumbers] and the [UseSerialPolymorphicNumbers] annotation. + */ + public var useSerialPolymorphicNumbers: Boolean? = null + */ /** * Registers a [subclass] [serializer] in the resulting module under the [base class][Base]. @@ -30,6 +39,19 @@ public class PolymorphicModuleBuilder @PublishedApi internal cons subclasses.add(subclass to serializer) } + /* + // TODO implement this or remove + /** + * Registers a [subclass] [serializer] in the resulting module under the [base class][Base] with the serial polymorphic number. + * If the class already has a [SerialPolymorphicNumber] annotation it's overridden by [serialPolymorphicNumber] here. + */ + public fun subclassWithSerialPolymorphicNumber( + subclass: KClass, serialPolymorphicNumber: Int, serializer: KSerializer + ) { + // TODO + } + */ + /** * Adds a default serializers provider associated with the given [baseClass] to the resulting module. * [defaultDeserializerProvider] is invoked when no polymorphic serializers associated with the `className` @@ -54,6 +76,16 @@ public class PolymorphicModuleBuilder @PublishedApi internal cons this.defaultDeserializerProvider = defaultDeserializerProvider } + /** + * TODO + */ + public fun defaultDeserializerForNumber(defaultDeserializerProviderForNumber: (serialPolymorphicNumber: Int?) -> DeserializationStrategy?) { + require(this.defaultDeserializerProviderForNumber == null) { + "Default deserializer provider for number is already registered for class $baseClass: ${this.defaultDeserializerProvider}" + } + this.defaultDeserializerProviderForNumber = defaultDeserializerProviderForNumber + } + /** * Adds a default deserializers provider associated with the given [baseClass] to the resulting module. * This function affect only deserialization process. To avoid confusion, it was deprecated and replaced with [defaultDeserializer]. @@ -102,6 +134,10 @@ public class PolymorphicModuleBuilder @PublishedApi internal cons if (defaultDeserializer != null) { builder.registerDefaultPolymorphicDeserializer(baseClass, defaultDeserializer, false) } + + defaultDeserializerProviderForNumber?.let { + builder.registerDefaultPolymorphicDeserializerForNumber(baseClass, it, false) + } } } diff --git a/core/commonMain/src/kotlinx/serialization/modules/SerializersModule.kt b/core/commonMain/src/kotlinx/serialization/modules/SerializersModule.kt index 8a9126d747..1aee35c1a2 100644 --- a/core/commonMain/src/kotlinx/serialization/modules/SerializersModule.kt +++ b/core/commonMain/src/kotlinx/serialization/modules/SerializersModule.kt @@ -62,6 +62,13 @@ public sealed class SerializersModule { @ExperimentalSerializationApi public abstract fun getPolymorphic(baseClass: KClass, serializedClassName: String?): DeserializationStrategy? + /** + * TODO + */ + public abstract fun getPolymorphicWithNumber( + baseClass: KClass, serializedNumber: Int? + ): DeserializationStrategy? + /** * Copies contents of this module to the given [collector]. */ @@ -76,7 +83,8 @@ public sealed class SerializersModule { level = DeprecationLevel.WARNING, replaceWith = ReplaceWith("EmptySerializersModule()")) @JsName("EmptySerializersModuleLegacyJs") // Compatibility with JS -public val EmptySerializersModule: SerializersModule = SerialModuleImpl(emptyMap(), emptyMap(), emptyMap(), emptyMap(), emptyMap()) +public val EmptySerializersModule: SerializersModule = + SerialModuleImpl(emptyMap(), emptyMap(), emptyMap(), emptyMap(), emptyMap(), emptyMap(), emptyMap()) /** * Returns a combination of two serial modules @@ -131,6 +139,15 @@ public infix fun SerializersModule.overwriteWith(other: SerializersModule): Seri ) { registerDefaultPolymorphicDeserializer(baseClass, defaultDeserializerProvider, allowOverwrite = true) } + + override fun polymorphicDefaultDeserializerForNumber( + baseClass: KClass, + defaultDeserializerProvider: PolymorphicDeserializerProviderForNumber + ) { + registerDefaultPolymorphicDeserializerForNumber( + baseClass, defaultDeserializerProvider, allowOverwrite = true + ) + } }) } @@ -147,7 +164,9 @@ internal class SerialModuleImpl( @JvmField val polyBase2Serializers: Map, Map, KSerializer<*>>>, private val polyBase2DefaultSerializerProvider: Map, PolymorphicSerializerProvider<*>>, private val polyBase2NamedSerializers: Map, Map>>, - private val polyBase2DefaultDeserializerProvider: Map, PolymorphicDeserializerProvider<*>> + private val polyBase2NumberedSerializers: Map, Map>>, + private val polyBase2DefaultDeserializerProvider: Map, PolymorphicDeserializerProvider<*>>, + private val polyBase2DefaultDeserializerProviderForNumber: Map, PolymorphicDeserializerProviderForNumber<*>> ) : SerializersModule() { override fun getPolymorphic(baseClass: KClass, value: T): SerializationStrategy? { @@ -167,6 +186,18 @@ internal class SerialModuleImpl( return (polyBase2DefaultDeserializerProvider[baseClass] as? PolymorphicDeserializerProvider)?.invoke(serializedClassName) } + override fun getPolymorphicWithNumber( + baseClass: KClass, serializedNumber: Int? + ): DeserializationStrategy? { + // Registered + val registered = polyBase2NumberedSerializers[baseClass]?.get(serializedNumber) as? KSerializer + if (registered != null) return registered + // Default + return (polyBase2DefaultDeserializerProviderForNumber[baseClass] as? PolymorphicDeserializerProviderForNumber)?.invoke( + serializedNumber + ) + } + override fun getContextual(kClass: KClass, typeArgumentsSerializers: List>): KSerializer? { return (class2ContextualFactory[kClass]?.invoke(typeArgumentsSerializers)) as? KSerializer? } @@ -203,6 +234,7 @@ internal class SerialModuleImpl( } internal typealias PolymorphicDeserializerProvider = (className: String?) -> DeserializationStrategy? +internal typealias PolymorphicDeserializerProviderForNumber = (serialPolymorphicNumber: Int?) -> DeserializationStrategy? internal typealias PolymorphicSerializerProvider = (value: Base) -> SerializationStrategy? /** This class is needed to support re-registering the same static (argless) serializers: diff --git a/core/commonMain/src/kotlinx/serialization/modules/SerializersModuleBuilders.kt b/core/commonMain/src/kotlinx/serialization/modules/SerializersModuleBuilders.kt index dfb9d819e3..f7026ee447 100644 --- a/core/commonMain/src/kotlinx/serialization/modules/SerializersModuleBuilders.kt +++ b/core/commonMain/src/kotlinx/serialization/modules/SerializersModuleBuilders.kt @@ -45,10 +45,14 @@ public fun EmptySerializersModule(): SerializersModule = @Suppress("DEPRECATION" @OptIn(ExperimentalSerializationApi::class) public class SerializersModuleBuilder @PublishedApi internal constructor() : SerializersModuleCollector { private val class2ContextualProvider: MutableMap, ContextualProvider> = hashMapOf() + //public var allUseSerialPolymorphicNumbers : Boolean? = null // TODO implement this or remove + //private val class2UseSerialPolymorphicNumbers : MutableMap, Boolean> = hashMapOf() // TODO implement this or remove private val polyBase2Serializers: MutableMap, MutableMap, KSerializer<*>>> = hashMapOf() private val polyBase2DefaultSerializerProvider: MutableMap, PolymorphicSerializerProvider<*>> = hashMapOf() private val polyBase2NamedSerializers: MutableMap, MutableMap>> = hashMapOf() + private val polyBase2NumberedSerializers: MutableMap, MutableMap>> = hashMapOf() private val polyBase2DefaultDeserializerProvider: MutableMap, PolymorphicDeserializerProvider<*>> = hashMapOf() + private val polyBase2DefaultDeserializerProviderForNumber: MutableMap, PolymorphicDeserializerProviderForNumber<*>> = hashMapOf() /** * Adds [serializer] associated with given [kClass] for contextual serialization. @@ -132,6 +136,16 @@ public class SerializersModuleBuilder @PublishedApi internal constructor() : Ser registerDefaultPolymorphicDeserializer(baseClass, defaultDeserializerProvider, false) } + /** + * TODO + */ + public override fun polymorphicDefaultDeserializerForNumber( + baseClass: KClass, + defaultDeserializerProvider: (serialPolymorphicNumber: Int?) -> DeserializationStrategy? + ) { + registerDefaultPolymorphicDeserializerForNumber(baseClass, defaultDeserializerProvider, false) + } + /** * Copies the content of [module] module into the current builder. */ @@ -174,6 +188,7 @@ public class SerializersModuleBuilder @PublishedApi internal constructor() : Ser internal fun registerDefaultPolymorphicDeserializer( baseClass: KClass, defaultDeserializerProvider: (className: String?) -> DeserializationStrategy?, + //defaultDeserializerProvider: PolymorphicDeserializerProvider, // this causes the build to fail on JS, but only when there is no trailing comment such as this one, which is strange allowOverwrite: Boolean ) { val previous = polyBase2DefaultDeserializerProvider[baseClass] @@ -183,6 +198,20 @@ public class SerializersModuleBuilder @PublishedApi internal constructor() : Ser polyBase2DefaultDeserializerProvider[baseClass] = defaultDeserializerProvider } + @JvmName("registerDefaultPolymorphicDeserializerForNumber") // Don't mangle method name for prettier stack traces + internal fun registerDefaultPolymorphicDeserializerForNumber( + baseClass: KClass, + defaultDeserializerProvider: (polymorphicSerialNumber: Int?) -> DeserializationStrategy?, + //defaultDeserializerProvider: PolymorphicDeserializerProviderForNumber, // this causes the build to fail on JS, but only when there is no trailing comment such as this one, which is strange + allowOverwrite: Boolean + ) { + val previous = polyBase2DefaultDeserializerProviderForNumber[baseClass] + if (previous != null && previous != defaultDeserializerProvider && !allowOverwrite) { + throw IllegalArgumentException("Default deserializers provider for $baseClass is already registered: $previous") + } + polyBase2DefaultDeserializerProviderForNumber[baseClass] = defaultDeserializerProvider + } + @JvmName("registerPolymorphicSerializer") // Don't mangle method name for prettier stack traces internal fun registerPolymorphicSerializer( baseClass: KClass, @@ -192,17 +221,21 @@ public class SerializersModuleBuilder @PublishedApi internal constructor() : Ser ) { // Check for overwrite val name = concreteSerializer.descriptor.serialName + val number = concreteSerializer.descriptor.serialPolymorphicNumberByBaseClass[baseClass] val baseClassSerializers = polyBase2Serializers.getOrPut(baseClass, ::hashMapOf) val previousSerializer = baseClassSerializers[concreteClass] val names = polyBase2NamedSerializers.getOrPut(baseClass, ::hashMapOf) + val numbers = polyBase2NumberedSerializers.getOrPut(baseClass, ::hashMapOf) if (allowOverwrite) { // Remove previous serializers from name mapping if (previousSerializer != null) { names.remove(previousSerializer.descriptor.serialName) + previousSerializer.descriptor.serialPolymorphicNumberByBaseClass[baseClass]?.let { numbers.remove(it) } } // Update mappings baseClassSerializers[concreteClass] = concreteSerializer names[name] = concreteSerializer + number?.let { numbers[it] = concreteSerializer } return } // Overwrite prohibited @@ -212,6 +245,7 @@ public class SerializersModuleBuilder @PublishedApi internal constructor() : Ser } else { // Cleanup name mapping names.remove(previousSerializer.descriptor.serialName) + previousSerializer.descriptor.serialPolymorphicNumberByBaseClass[baseClass]?.let { numbers.remove(it) } } } val previousByName = names[name] @@ -222,14 +256,45 @@ public class SerializersModuleBuilder @PublishedApi internal constructor() : Ser "have the same serial name '$name': '$concreteClass' and '$conflictingClass'" ) } + number?.let { + val previousByNumber = numbers[it] + if (previousByNumber != null) { + val conflictingClass = + polyBase2Serializers[baseClass]!!.asSequence().find { it.value === previousByNumber } + throw IllegalArgumentException( + "Multiple polymorphic serializers for base class '$baseClass' " + + "have the same polymorphic serial number '$number': '$concreteClass' and '$conflictingClass'" + ) + } + } // Overwrite if no conflicts baseClassSerializers[concreteClass] = concreteSerializer names[name] = concreteSerializer + number?.let { numbers[it] = concreteSerializer } + } + + /* + // TODO implement this or remove? + internal fun registerUseSerialPolymorphicNumbers(baseClass: KClass<*>, useSerialPolymorphicNumbers: Boolean?) { + // TODO what about the annotation? baseDescriptor? + if (useSerialPolymorphicNumbers !== null) + class2UseSerialPolymorphicNumbers.put(baseClass, useSerialPolymorphicNumbers) + else + class2UseSerialPolymorphicNumbers.remove(baseClass) } + */ @PublishedApi internal fun build(): SerializersModule = - SerialModuleImpl(class2ContextualProvider, polyBase2Serializers, polyBase2DefaultSerializerProvider, polyBase2NamedSerializers, polyBase2DefaultDeserializerProvider) + SerialModuleImpl( + class2ContextualProvider, + polyBase2Serializers, + polyBase2DefaultSerializerProvider, + polyBase2NamedSerializers, + polyBase2NumberedSerializers, + polyBase2DefaultDeserializerProvider, + polyBase2DefaultDeserializerProviderForNumber + ) } /** diff --git a/core/commonMain/src/kotlinx/serialization/modules/SerializersModuleCollector.kt b/core/commonMain/src/kotlinx/serialization/modules/SerializersModuleCollector.kt index c33d45a4c2..fc0c7d12ae 100644 --- a/core/commonMain/src/kotlinx/serialization/modules/SerializersModuleCollector.kt +++ b/core/commonMain/src/kotlinx/serialization/modules/SerializersModuleCollector.kt @@ -72,7 +72,7 @@ public interface SerializersModuleCollector { */ public fun polymorphicDefaultDeserializer( baseClass: KClass, - defaultDeserializerProvider: (className: String?) -> DeserializationStrategy? + defaultDeserializerProvider: PolymorphicDeserializerProvider ) /** @@ -101,4 +101,9 @@ public interface SerializersModuleCollector { ) { polymorphicDefaultDeserializer(baseClass, defaultDeserializerProvider) } + + public fun polymorphicDefaultDeserializerForNumber( + baseClass: KClass, + defaultDeserializerProvider: PolymorphicDeserializerProviderForNumber + ) } diff --git a/formats/json-tests/commonTest/src/kotlinx/serialization/SerialPolymorphicNumberTest.kt b/formats/json-tests/commonTest/src/kotlinx/serialization/SerialPolymorphicNumberTest.kt new file mode 100644 index 0000000000..5595a6cf65 --- /dev/null +++ b/formats/json-tests/commonTest/src/kotlinx/serialization/SerialPolymorphicNumberTest.kt @@ -0,0 +1,94 @@ +/* + * Copyright 2017-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.serialization + +import kotlinx.serialization.json.* +import kotlinx.serialization.modules.* +import kotlinx.serialization.test.* +import kotlin.test.* + +class SerialPolymorphicNumberTest { + @Serializable + @UseSerialPolymorphicNumbers + sealed class Sealed1 { + @Serializable + @SerialPolymorphicNumber(Sealed1::class, 1) + data class Case1(val property: Int) : Sealed1() + + @Serializable + @SerialPolymorphicNumber(Sealed1::class, 2) + object Case2 : Sealed1() + } + + @Serializable + sealed class Sealed2 { + @Serializable + @SerialPolymorphicNumber(Sealed2::class, 1) + object Case : Sealed2() + } + + @Serializable + @UseSerialPolymorphicNumbers + sealed class Sealed3 { + @Serializable + object Case : Sealed3() + } + + @Serializable + @UseSerialPolymorphicNumbers + sealed class Sealed4 { + @Serializable + @UseSerialPolymorphicNumbers + sealed class Sealed41 : Sealed4() { + @Serializable + @SerialPolymorphicNumber(Sealed4::class, 1) + @SerialPolymorphicNumber(Sealed41::class, 2) + object Case : Sealed41() + } + } + + @Test + fun testSealed() { + testConversion(Sealed1.Case1(1), """{"type":1,"property":1}""") + testConversion(Sealed1.Case2, """{"type":2}""") + testConversion(Sealed2.Case, """{"type":"${Sealed2.Case.serializer().descriptor.serialName}"}""") + assertFailsWith(SerializationException::class) { + Json.encodeToString(Sealed3.Case) + } + assertFailsWith(SerializationException::class) { + Json.decodeFromString("{}") + } + testConversion(Sealed4.Sealed41.Case, """{"type":1}""") + testConversion(Sealed4.Sealed41.Case, """{"type":2}""") + } + + @Serializable + @UseSerialPolymorphicNumbers + abstract class Abstract { + @Serializable + @SerialPolymorphicNumber(Abstract::class, 1) + object Case : Abstract() + + @Serializable + data class Default(val type: Int?) : Abstract() + } + + val json = Json { + serializersModule = SerializersModule { + polymorphic(Abstract::class) { + subclass(Abstract.Case::class) + defaultDeserializerForNumber { + Abstract.Default.serializer() + } + } + } + } + + @Test + fun testPolymorphicModule() { + testConversion(json, Abstract.Case, """{"type":1}""") + assertEquals(Abstract.Default(0), json.decodeFromString("""{"type":0}""")) + } +} \ No newline at end of file diff --git a/formats/json-tests/commonTest/src/kotlinx/serialization/test/JsonTestHelpers.kt b/formats/json-tests/commonTest/src/kotlinx/serialization/test/JsonTestHelpers.kt new file mode 100644 index 0000000000..c6274036b7 --- /dev/null +++ b/formats/json-tests/commonTest/src/kotlinx/serialization/test/JsonTestHelpers.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2017-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.serialization.test + +import kotlinx.serialization.* +import kotlinx.serialization.json.* +import kotlin.test.* + +inline fun testConversion(json: Json, data: T, expectedString: String) { + assertEquals(expectedString, json.encodeToString(data)) + assertEquals(data, json.decodeFromString(expectedString)) + + jvmOnly { + assertEquals(expectedString, json.encodeViaStream(serializer(), data)) + assertEquals(data, json.decodeViaStream(serializer(), expectedString)) + } + + val jsonElement = json.encodeToJsonElement(data) + assertEquals(expectedString, jsonElement.toString()) + assertEquals(data, json.decodeFromJsonElement(jsonElement)) +} + +inline fun testConversion(data: T, expectedString: String) = + testConversion(Json, data, expectedString) diff --git a/formats/json/commonMain/src/kotlinx/serialization/json/internal/Polymorphic.kt b/formats/json/commonMain/src/kotlinx/serialization/json/internal/Polymorphic.kt index 636f340ddb..8522f5ed27 100644 --- a/formats/json/commonMain/src/kotlinx/serialization/json/internal/Polymorphic.kt +++ b/formats/json/commonMain/src/kotlinx/serialization/json/internal/Polymorphic.kt @@ -9,14 +9,13 @@ import kotlinx.serialization.* import kotlinx.serialization.descriptors.* import kotlinx.serialization.internal.* import kotlinx.serialization.json.* -import kotlinx.serialization.modules.* -import kotlin.jvm.* @Suppress("UNCHECKED_CAST") internal inline fun JsonEncoder.encodePolymorphically( serializer: SerializationStrategy, value: T, - ifPolymorphic: (String) -> Unit + ifHasBaseClassDiscriminator: (String) -> Unit, + ifUseSerialPolymorphicNumber : (Int) -> Unit ) { if (json.configuration.useArrayPolymorphism) { serializer.serialize(this, value) @@ -42,7 +41,11 @@ internal inline fun JsonEncoder.encodePolymorphically( actual as SerializationStrategy } else serializer - if (baseClassDiscriminator != null) ifPolymorphic(baseClassDiscriminator) + if (baseClassDiscriminator != null) ifHasBaseClassDiscriminator(baseClassDiscriminator) + + if (isPolymorphicSerializer && serializer.descriptor.useSerialPolymorphicNumbers) + ifUseSerialPolymorphicNumber(actualSerializer.descriptor.getSerialPolymorphicNumberByBaseClass((serializer as AbstractPolymorphicSerializer).baseClass)) + actualSerializer.serialize(this, value) } @@ -79,11 +82,18 @@ internal fun JsonDecoder.decodeSerializableValuePolymorphic(deserializer: De val discriminator = deserializer.descriptor.classDiscriminator(json) val jsonTree = cast(decodeJsonElement(), deserializer.descriptor) - val type = jsonTree[discriminator]?.jsonPrimitive?.contentOrNull // differentiate between `"type":"null"` and `"type":null`. + val useSerialPolymorphicNumbers = deserializer.descriptor.useSerialPolymorphicNumbers + val type = if (useSerialPolymorphicNumbers) + jsonTree[discriminator]?.jsonPrimitive?.intOrNull + else + jsonTree[discriminator]?.jsonPrimitive?.contentOrNull // differentiate between `"type":"null"` and `"type":null`. @Suppress("UNCHECKED_CAST") val actualSerializer = try { - deserializer.findPolymorphicSerializer(this, type) + if (useSerialPolymorphicNumbers) + deserializer.findPolymorphicSerializerWithNumber(this, type as Int?) + else + deserializer.findPolymorphicSerializer(this, type as String?) } catch (it: SerializationException) { // Wrap SerializationException into JsonDecodingException to preserve input throw JsonDecodingException(-1, it.message!!, jsonTree.toString()) } as DeserializationStrategy diff --git a/formats/json/commonMain/src/kotlinx/serialization/json/internal/PolymorphismValidator.kt b/formats/json/commonMain/src/kotlinx/serialization/json/internal/PolymorphismValidator.kt index e4606fae05..2b8126c366 100644 --- a/formats/json/commonMain/src/kotlinx/serialization/json/internal/PolymorphismValidator.kt +++ b/formats/json/commonMain/src/kotlinx/serialization/json/internal/PolymorphismValidator.kt @@ -87,4 +87,11 @@ internal class PolymorphismValidator( ) { // Nothing here } + + override fun polymorphicDefaultDeserializerForNumber( + baseClass: KClass, + defaultDeserializerProvider: (serialPolymorphicNumber: Int?) -> DeserializationStrategy? + ) { + // Nothing here + } } diff --git a/formats/json/commonMain/src/kotlinx/serialization/json/internal/StreamingJsonEncoder.kt b/formats/json/commonMain/src/kotlinx/serialization/json/internal/StreamingJsonEncoder.kt index cf562de5c8..f45c804c47 100644 --- a/formats/json/commonMain/src/kotlinx/serialization/json/internal/StreamingJsonEncoder.kt +++ b/formats/json/commonMain/src/kotlinx/serialization/json/internal/StreamingJsonEncoder.kt @@ -43,6 +43,7 @@ internal class StreamingJsonEncoder( // Forces serializer to wrap all values into quotes private var forceQuoting: Boolean = false private var polymorphicDiscriminator: String? = null + private var serialPolymorphicNumber : Int? = null init { val i = mode.ordinal @@ -61,9 +62,7 @@ internal class StreamingJsonEncoder( } override fun encodeSerializableValue(serializer: SerializationStrategy, value: T) { - encodePolymorphically(serializer, value) { - polymorphicDiscriminator = it - } + encodePolymorphically(serializer, value, { polymorphicDiscriminator = it }, { serialPolymorphicNumber = it }) } private fun encodeTypeInfo(descriptor: SerialDescriptor) { @@ -71,7 +70,11 @@ internal class StreamingJsonEncoder( encodeString(polymorphicDiscriminator!!) composer.print(COLON) composer.space() - encodeString(descriptor.serialName) + serialPolymorphicNumber?.let { + encodeInt(it) + serialPolymorphicNumber = null + } + ?: encodeString(descriptor.serialName) } override fun beginStructure(descriptor: SerialDescriptor): CompositeEncoder { diff --git a/formats/json/commonMain/src/kotlinx/serialization/json/internal/TreeJsonEncoder.kt b/formats/json/commonMain/src/kotlinx/serialization/json/internal/TreeJsonEncoder.kt index 5e3c808689..ac532f5f16 100644 --- a/formats/json/commonMain/src/kotlinx/serialization/json/internal/TreeJsonEncoder.kt +++ b/formats/json/commonMain/src/kotlinx/serialization/json/internal/TreeJsonEncoder.kt @@ -35,6 +35,7 @@ private sealed class AbstractJsonTreeEncoder( protected val configuration = json.configuration private var polymorphicDiscriminator: String? = null + private var serialPolymorphicNumber: Int? = null override fun elementName(descriptor: SerialDescriptor, index: Int): String = descriptor.getJsonElementName(json, index) @@ -77,7 +78,8 @@ private sealed class AbstractJsonTreeEncoder( override fun encodeSerializableValue(serializer: SerializationStrategy, value: T) { // Writing non-structured data (i.e. primitives) on top-level (e.g. without any tag) requires special output if (currentTagOrNull != null || !serializer.descriptor.carrierDescriptor(serializersModule).requiresTopLevelTag) { - encodePolymorphically(serializer, value) { polymorphicDiscriminator = it } + encodePolymorphically( + serializer, value, { polymorphicDiscriminator = it }, { serialPolymorphicNumber = it }) } else JsonPrimitiveEncoder(json, nodeConsumer).apply { encodeSerializableValue(serializer, value) } @@ -149,7 +151,14 @@ private sealed class AbstractJsonTreeEncoder( } if (polymorphicDiscriminator != null) { - encoder.putElement(polymorphicDiscriminator!!, JsonPrimitive(descriptor.serialName)) + encoder.putElement( + polymorphicDiscriminator!!, + serialPolymorphicNumber?.let { + serialPolymorphicNumber = null + JsonPrimitive(it) + } + ?: JsonPrimitive(descriptor.serialName) + ) polymorphicDiscriminator = null } diff --git a/formats/json/jsMain/src/kotlinx/serialization/json/internal/DynamicEncoders.kt b/formats/json/jsMain/src/kotlinx/serialization/json/internal/DynamicEncoders.kt index 4c4841d47d..32b0baa094 100644 --- a/formats/json/jsMain/src/kotlinx/serialization/json/internal/DynamicEncoders.kt +++ b/formats/json/jsMain/src/kotlinx/serialization/json/internal/DynamicEncoders.kt @@ -62,6 +62,7 @@ private class DynamicObjectEncoder( * Flag of usage polymorphism with discriminator attribute */ private var polymorphicDiscriminator: String? = null + private var serialPolymorphicNumber: Int? = null private object NoOutputMark @@ -183,9 +184,7 @@ private class DynamicObjectEncoder( private fun isNotStructured() = result === NoOutputMark override fun encodeSerializableValue(serializer: SerializationStrategy, value: T) { - encodePolymorphically(serializer, value) { - polymorphicDiscriminator = it - } + encodePolymorphically(serializer, value, { polymorphicDiscriminator = it }, { serialPolymorphicNumber = it }) } override fun beginStructure(descriptor: SerialDescriptor): CompositeEncoder { @@ -208,8 +207,9 @@ private class DynamicObjectEncoder( enterNode(child, newMode) } - if (polymorphicDiscriminator != null) { - current.jsObject[polymorphicDiscriminator!!] = descriptor.serialName + polymorphicDiscriminator?.let { + current.jsObject[it] = + serialPolymorphicNumber?.also { serialPolymorphicNumber = null } ?: descriptor.serialName polymorphicDiscriminator = null } diff --git a/formats/protobuf/commonTest/src/kotlinx/serialization/protobuf/SerialPolymorphicNumberTest.kt b/formats/protobuf/commonTest/src/kotlinx/serialization/protobuf/SerialPolymorphicNumberTest.kt new file mode 100644 index 0000000000..a014dd7f73 --- /dev/null +++ b/formats/protobuf/commonTest/src/kotlinx/serialization/protobuf/SerialPolymorphicNumberTest.kt @@ -0,0 +1,111 @@ +/* + * Copyright 2017-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.serialization.protobuf + +import kotlinx.serialization.* +import kotlinx.serialization.modules.* +import kotlin.test.* + +/** + * Copied and adapted from [kotlinx.serialization.SerialPolymorphicNumberTest]. + */ +class SerialPolymorphicNumberTest { + inline fun testConversion(protoBuf: ProtoBuf, data: T, expectedHexString: String) { + val string = protoBuf.encodeToHexString(data) + assertEquals(expectedHexString, string) + assertEquals(data, protoBuf.decodeFromHexString(string)) + } + + inline fun testConversion(data: T, expectedHexString: String) = + testConversion(ProtoBuf, data, expectedHexString) + + @Serializable + @UseSerialPolymorphicNumbers + sealed class Sealed1 { + @Serializable + @SerialPolymorphicNumber(Sealed1::class, 1) + data class Case1(val property: Int) : Sealed1() + + @Serializable + @SerialPolymorphicNumber(Sealed1::class, 2) + object Case2 : Sealed1() + } + + @Serializable + sealed class Sealed2 { + @Serializable + @SerialPolymorphicNumber(Sealed2::class, 1) + object Case : Sealed2() + } + + @Serializable + @UseSerialPolymorphicNumbers + sealed class Sealed3 { + @Serializable + object Case : Sealed3() + } + + @Serializable + @UseSerialPolymorphicNumbers + sealed class Sealed4 { + @Serializable + @UseSerialPolymorphicNumbers + sealed class Sealed41 : Sealed4() { + @Serializable + @SerialPolymorphicNumber(Sealed4::class, 1) + @SerialPolymorphicNumber(Sealed41::class, 2) + object Case : Sealed41() + } + } + + @Test + fun testSealed() { + testConversion(Sealed1.Case1(1), "080112020801") + testConversion(Sealed1.Case2, "08021200") + run { + val serialName = Sealed2.Case.serializer().descriptor.serialName + testConversion( + Sealed2.Case, + "0a" + serialName.length.toByte().toHexString() + serialName.encodeToByteArray().toHexString() + "1200" + ) + } + assertFailsWith(SerializationException::class) { + ProtoBuf.encodeToHexString(Sealed3.Case) + } + assertFailsWith(SerializationException::class) { + ProtoBuf.decodeFromHexString("08011200") + } + testConversion(Sealed4.Sealed41.Case, "08011200") + testConversion(Sealed4.Sealed41.Case, "08021200") + } + + @Serializable + @UseSerialPolymorphicNumbers + abstract class Abstract { + @Serializable + @SerialPolymorphicNumber(Abstract::class, 1) + object Case : Abstract() + + @Serializable + object Default : Abstract() + } + + val protoBuf = ProtoBuf { + serializersModule = SerializersModule { + polymorphic(Abstract::class) { + subclass(Abstract.Case::class) + defaultDeserializerForNumber { + Abstract.Default.serializer() + } + } + } + } + + @Test + fun testPolymorphicModule() { + testConversion(protoBuf, Abstract.Case, "08011200") + assertEquals(Abstract.Default, protoBuf.decodeFromHexString("08001200")) + } +} \ No newline at end of file