From 4ed38d7d1b5845bdbc7a6114ca5e31e9172c01b6 Mon Sep 17 00:00:00 2001 From: Shreck Ye Date: Sat, 17 Feb 2024 20:26:46 +0800 Subject: [PATCH 01/12] Initially add the annotations and refactor the serializers, descriptors, and serializers modules for serializing type numbers in polymorphic serialization without any support from the serialization plugin yet --- .../src/kotlinx/serialization/Annotations.kt | 21 ++++++ .../serialization/PolymorphicSerializer.kt | 7 ++ .../kotlinx/serialization/SealedSerializer.kt | 26 +++++++ .../descriptors/SerialDescriptor.kt | 13 ++++ .../descriptors/SerialDescriptors.kt | 18 ++++- .../internal/AbstractPolymorphicSerializer.kt | 55 +++++++++++++-- .../modules/PolymorphicModuleBuilder.kt | 34 +++++++++- .../modules/SerializersModule.kt | 63 ++++++++++++++++- .../modules/SerializersModuleBuilders.kt | 67 ++++++++++++++++++- .../modules/SerializersModuleCollector.kt | 21 +++++- .../json/internal/PolymorphismValidator.kt | 7 ++ 11 files changed, 322 insertions(+), 10 deletions(-) diff --git a/core/commonMain/src/kotlinx/serialization/Annotations.kt b/core/commonMain/src/kotlinx/serialization/Annotations.kt index 67104dc3c6..20f7b962e3 100644 --- a/core/commonMain/src/kotlinx/serialization/Annotations.kt +++ b/core/commonMain/src/kotlinx/serialization/Annotations.kt @@ -152,6 +152,27 @@ public annotation class Serializer( // @Retention(AnnotationRetention.RUNTIME) still runtime, but KT-41082 public annotation class SerialName(val value: String) +/** + * Requires all subclasses to use [SerialPolymorphicNumber]. + */ +@MustBeDocumented +@Target(AnnotationTarget.CLASS) +@Repeatable +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 [value]. + * + * Using a number instead of a string shortens the size of the serialized message, especially in a binary format. + */ +@MustBeDocumented +@Target(AnnotationTarget.CLASS) +@Repeatable +public annotation class SerialPolymorphicNumber(val baseClass: KClass<*>, val value: 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..eb6f29cf54 100644 --- a/core/commonMain/src/kotlinx/serialization/SealedSerializer.kt +++ b/core/commonMain/src/kotlinx/serialization/SealedSerializer.kt @@ -117,6 +117,7 @@ public class SealedClassSerializer( private val class2Serializer: Map, KSerializer> private val serialName2Serializer: Map> + private val serialPolymorphicNumber2Serializer : Map>? init { if (subclasses.size != subclassSerializers.size) { @@ -127,6 +128,7 @@ public class SealedClassSerializer( // Plugin should produce identical serializers, although they are not always strictly equal (e.g. new ObjectSerializer // may be created every time) class2Serializer = subclasses.zip(subclassSerializers).toMap() + serialName2Serializer = class2Serializer.entries.groupingBy { it.value.descriptor.serialName } .aggregate, KSerializer>, String, Map.Entry, KSerializer>> { key, accumulator, element, _ -> @@ -138,6 +140,23 @@ public class SealedClassSerializer( } element }.mapValues { it.value.value } + + serialPolymorphicNumber2Serializer = if (descriptor.useSerialPolymorphicNumbers) + class2Serializer.entries.groupingBy { + it.value.descriptor.serialPolymorphicNumberByBaseClass.getValue(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( @@ -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..94edbce928 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. @@ -195,6 +196,18 @@ public interface SerialDescriptor { @ExperimentalSerializationApi public val elementsCount: Int + /** + * TODO + */ + @ExperimentalSerializationApi + public val useSerialPolymorphicNumbers: Boolean get() = false + + /** + * TODO + */ + @ExperimentalSerializationApi + public val serialPolymorphicNumberByBaseClass: Map, Int> get() = emptyMap() + /** * Returns serial annotations of the associated class. * Serial annotations can be used to specify an additional metadata that may be used during serialization. diff --git a/core/commonMain/src/kotlinx/serialization/descriptors/SerialDescriptors.kt b/core/commonMain/src/kotlinx/serialization/descriptors/SerialDescriptors.kt index cb380aafc0..df5eb3c6c8 100644 --- a/core/commonMain/src/kotlinx/serialization/descriptors/SerialDescriptors.kt +++ b/core/commonMain/src/kotlinx/serialization/descriptors/SerialDescriptors.kt @@ -54,6 +54,8 @@ import kotlin.reflect.* public fun buildClassSerialDescriptor( serialName: String, vararg typeParameters: SerialDescriptor, + useSerialPolymorphicNumbers: Boolean = false, + serialPolymorphicNumbers: Map, Int> = emptyMap(), builderAction: ClassSerialDescriptorBuilder.() -> Unit = {} ): SerialDescriptor { require(serialName.isNotBlank()) { "Blank serial names are prohibited" } @@ -64,6 +66,8 @@ public fun buildClassSerialDescriptor( StructureKind.CLASS, sdBuilder.elementNames.size, typeParameters.toList(), + useSerialPolymorphicNumbers, + serialPolymorphicNumbers, sdBuilder ) } @@ -140,13 +144,23 @@ public fun buildSerialDescriptor( serialName: String, kind: SerialKind, vararg typeParameters: SerialDescriptor, + useSerialPolymorphicNumbers: Boolean = false, + serialPolymorphicNumbers: Map, Int> = emptyMap(), builder: ClassSerialDescriptorBuilder.() -> Unit = {} ): SerialDescriptor { require(serialName.isNotBlank()) { "Blank serial names are prohibited" } require(kind != StructureKind.CLASS) { "For StructureKind.CLASS please use 'buildClassSerialDescriptor' instead" } val sdBuilder = ClassSerialDescriptorBuilder(serialName) sdBuilder.builder() - return SerialDescriptorImpl(serialName, kind, sdBuilder.elementNames.size, typeParameters.toList(), sdBuilder) + return SerialDescriptorImpl( + serialName, + kind, + sdBuilder.elementNames.size, + typeParameters.toList(), + useSerialPolymorphicNumbers, + serialPolymorphicNumbers, + sdBuilder + ) } @@ -309,6 +323,8 @@ internal class SerialDescriptorImpl( override val kind: SerialKind, override val elementsCount: Int, typeParameters: List, + override val useSerialPolymorphicNumbers : Boolean, + override val serialPolymorphicNumberByBaseClass : Map, Int>, builder: ClassSerialDescriptorBuilder ) : SerialDescriptor, CachedNames { diff --git a/core/commonMain/src/kotlinx/serialization/internal/AbstractPolymorphicSerializer.kt b/core/commonMain/src/kotlinx/serialization/internal/AbstractPolymorphicSerializer.kt index 26d3b5e27f..7b0c3bfe1a 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,22 @@ 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, + // it seems not possible to cache this with the current implementation that serializers are completely separated from serializers modules + actualSerializer.descriptor.serialPolymorphicNumberByBaseClass.getValue(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 +58,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 +104,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 +140,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 $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}' 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/modules/PolymorphicModuleBuilder.kt b/core/commonMain/src/kotlinx/serialization/modules/PolymorphicModuleBuilder.kt index 1b8d431e1a..69552b4d00 100644 --- a/core/commonMain/src/kotlinx/serialization/modules/PolymorphicModuleBuilder.kt +++ b/core/commonMain/src/kotlinx/serialization/modules/PolymorphicModuleBuilder.kt @@ -21,7 +21,15 @@ 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 + + /* + // 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 +38,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 +75,17 @@ public class PolymorphicModuleBuilder @PublishedApi internal cons this.defaultDeserializerProvider = defaultDeserializerProvider } + /* + // TODO remove + @Deprecated( + "Deprecated in favor of function with new `PolymorphicDeserializerProvider` API", + level = DeprecationLevel.WARNING // Since TODO. Raise to ERROR in TODO, hide in TODO + ) + public fun defaultDeserializer(defaultDeserializerProvider: (className: String?) -> DeserializationStrategy?) { + defaultDeserializer(defaultDeserializerProvider.toNewApi()) + } + */ + /** * 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]. diff --git a/core/commonMain/src/kotlinx/serialization/modules/SerializersModule.kt b/core/commonMain/src/kotlinx/serialization/modules/SerializersModule.kt index 8a9126d747..efcc391baf 100644 --- a/core/commonMain/src/kotlinx/serialization/modules/SerializersModule.kt +++ b/core/commonMain/src/kotlinx/serialization/modules/SerializersModule.kt @@ -62,6 +62,18 @@ public sealed class SerializersModule { @ExperimentalSerializationApi public abstract fun getPolymorphic(baseClass: KClass, serializedClassName: String?): DeserializationStrategy? + /** + * TODO + */ + public abstract fun getPolymorphicWithNumber( + baseClass: KClass, serializedNumber: Int? + ): DeserializationStrategy? + + // TODO remove + // TODO old design for cashing serializers by number in `AbstractPolymorphicSerializer` which probably doesn't work and is not needed + @ExperimentalSerializationApi + public abstract fun getPolymorphicForAllSubclasses(baseClass: KClass): Map, KSerializer> + /** * Copies contents of this module to the given [collector]. */ @@ -76,7 +88,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 +144,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 +169,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>>, // TODO remove + private val polyBase2DefaultDeserializerProvider: Map, PolymorphicDeserializerProvider<*>>, + private val polyBase2DefaultDeserializerProviderForNumber: Map, PolymorphicDeserializerProviderForNumber<*>> ) : SerializersModule() { override fun getPolymorphic(baseClass: KClass, value: T): SerializationStrategy? { @@ -167,6 +191,23 @@ 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 + ) + } + + // TODO remove + @ExperimentalSerializationApi + override fun getPolymorphicForAllSubclasses(baseClass: KClass): Map, KSerializer> = + polyBase2Serializers.getValue(baseClass) as Map, KSerializer> + override fun getContextual(kClass: KClass, typeArgumentsSerializers: List>): KSerializer? { return (class2ContextualFactory[kClass]?.invoke(typeArgumentsSerializers)) as? KSerializer? } @@ -202,7 +243,25 @@ internal class SerialModuleImpl( } } +/* +// TODO remove old suboptimal design + +public interface PolymorphicDeserializerProvider { + public fun fromClassName(className: String?): DeserializationStrategy? + public fun fromSerialPolymorphicNumber(serialPolymorphicNumber: Int?): DeserializationStrategy? +} + +internal fun ((className: String?) -> DeserializationStrategy?).toNewApi() = + object : PolymorphicDeserializerProvider { + override fun fromClassName(className: String?): DeserializationStrategy? = + this@toNewApi(className) + + override fun fromSerialPolymorphicNumber(serialPolymorphicNumber: Int?): DeserializationStrategy? = + throw AssertionError("This instance should only be created by legacy code therefore this function shouldn't be invoked here.") + } +*/ 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..2503b5c24b 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, 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, + 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) + numbers.remove(previousSerializer.descriptor.serialPolymorphicNumberByBaseClass[baseClass]) } // 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) + numbers.remove(previousSerializer.descriptor.serialPolymorphicNumberByBaseClass[baseClass]) } } 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..8421cab2bb 100644 --- a/core/commonMain/src/kotlinx/serialization/modules/SerializersModuleCollector.kt +++ b/core/commonMain/src/kotlinx/serialization/modules/SerializersModuleCollector.kt @@ -59,6 +59,20 @@ public interface SerializersModuleCollector { defaultSerializerProvider: (value: Base) -> SerializationStrategy? ) + /* + // TODO remove + @Deprecated( + "Deprecated in favor of function with new `PolymorphicDeserializerProvider` API", + level = DeprecationLevel.WARNING // Since TODO. Raise to ERROR in TODO, hide in TODO + ) + public fun polymorphicDefaultDeserializer( + baseClass: KClass, + defaultDeserializerProvider: (className: String?) -> DeserializationStrategy? + ) { + polymorphicDefaultDeserializer(baseClass, defaultDeserializerProvider.toNewApi()) + } + */ + /** * Accept a default deserializer provider, associated with the [baseClass] for polymorphic deserialization. * [defaultDeserializerProvider] is invoked when no polymorphic serializers associated with the `className` @@ -72,7 +86,7 @@ public interface SerializersModuleCollector { */ public fun polymorphicDefaultDeserializer( baseClass: KClass, - defaultDeserializerProvider: (className: String?) -> DeserializationStrategy? + defaultDeserializerProvider: PolymorphicDeserializerProvider ) /** @@ -101,4 +115,9 @@ public interface SerializersModuleCollector { ) { polymorphicDefaultDeserializer(baseClass, defaultDeserializerProvider) } + + public fun polymorphicDefaultDeserializerForNumber( + baseClass: KClass, + defaultDeserializerProvider: PolymorphicDeserializerProviderForNumber + ) } 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 + } } From d8e06b266dd9d75293857cf603171037e18d5d2a Mon Sep 17 00:00:00 2001 From: Shreck Ye Date: Sun, 18 Feb 2024 11:16:22 +0800 Subject: [PATCH 02/12] Remove unused code introduced in the previous commit --- .../modules/PolymorphicModuleBuilder.kt | 11 --------- .../modules/SerializersModule.kt | 23 +++---------------- .../modules/SerializersModuleCollector.kt | 14 ----------- 3 files changed, 3 insertions(+), 45 deletions(-) diff --git a/core/commonMain/src/kotlinx/serialization/modules/PolymorphicModuleBuilder.kt b/core/commonMain/src/kotlinx/serialization/modules/PolymorphicModuleBuilder.kt index 69552b4d00..1b1c4e01c5 100644 --- a/core/commonMain/src/kotlinx/serialization/modules/PolymorphicModuleBuilder.kt +++ b/core/commonMain/src/kotlinx/serialization/modules/PolymorphicModuleBuilder.kt @@ -75,17 +75,6 @@ public class PolymorphicModuleBuilder @PublishedApi internal cons this.defaultDeserializerProvider = defaultDeserializerProvider } - /* - // TODO remove - @Deprecated( - "Deprecated in favor of function with new `PolymorphicDeserializerProvider` API", - level = DeprecationLevel.WARNING // Since TODO. Raise to ERROR in TODO, hide in TODO - ) - public fun defaultDeserializer(defaultDeserializerProvider: (className: String?) -> DeserializationStrategy?) { - defaultDeserializer(defaultDeserializerProvider.toNewApi()) - } - */ - /** * 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]. diff --git a/core/commonMain/src/kotlinx/serialization/modules/SerializersModule.kt b/core/commonMain/src/kotlinx/serialization/modules/SerializersModule.kt index efcc391baf..956dadf8a3 100644 --- a/core/commonMain/src/kotlinx/serialization/modules/SerializersModule.kt +++ b/core/commonMain/src/kotlinx/serialization/modules/SerializersModule.kt @@ -69,7 +69,7 @@ public sealed class SerializersModule { baseClass: KClass, serializedNumber: Int? ): DeserializationStrategy? - // TODO remove + // TODO remove or use // TODO old design for cashing serializers by number in `AbstractPolymorphicSerializer` which probably doesn't work and is not needed @ExperimentalSerializationApi public abstract fun getPolymorphicForAllSubclasses(baseClass: KClass): Map, KSerializer> @@ -169,7 +169,7 @@ internal class SerialModuleImpl( @JvmField val polyBase2Serializers: Map, Map, KSerializer<*>>>, private val polyBase2DefaultSerializerProvider: Map, PolymorphicSerializerProvider<*>>, private val polyBase2NamedSerializers: Map, Map>>, - private val polyBase2NumberedSerializers: Map, Map>>, // TODO remove + private val polyBase2NumberedSerializers: Map, Map>>, private val polyBase2DefaultDeserializerProvider: Map, PolymorphicDeserializerProvider<*>>, private val polyBase2DefaultDeserializerProviderForNumber: Map, PolymorphicDeserializerProviderForNumber<*>> ) : SerializersModule() { @@ -203,7 +203,7 @@ internal class SerialModuleImpl( ) } - // TODO remove + // TODO remove or use @ExperimentalSerializationApi override fun getPolymorphicForAllSubclasses(baseClass: KClass): Map, KSerializer> = polyBase2Serializers.getValue(baseClass) as Map, KSerializer> @@ -243,23 +243,6 @@ internal class SerialModuleImpl( } } -/* -// TODO remove old suboptimal design - -public interface PolymorphicDeserializerProvider { - public fun fromClassName(className: String?): DeserializationStrategy? - public fun fromSerialPolymorphicNumber(serialPolymorphicNumber: Int?): DeserializationStrategy? -} - -internal fun ((className: String?) -> DeserializationStrategy?).toNewApi() = - object : PolymorphicDeserializerProvider { - override fun fromClassName(className: String?): DeserializationStrategy? = - this@toNewApi(className) - - override fun fromSerialPolymorphicNumber(serialPolymorphicNumber: Int?): DeserializationStrategy? = - throw AssertionError("This instance should only be created by legacy code therefore this function shouldn't be invoked here.") - } -*/ internal typealias PolymorphicDeserializerProvider = (className: String?) -> DeserializationStrategy? internal typealias PolymorphicDeserializerProviderForNumber = (serialPolymorphicNumber: Int?) -> DeserializationStrategy? internal typealias PolymorphicSerializerProvider = (value: Base) -> SerializationStrategy? diff --git a/core/commonMain/src/kotlinx/serialization/modules/SerializersModuleCollector.kt b/core/commonMain/src/kotlinx/serialization/modules/SerializersModuleCollector.kt index 8421cab2bb..fc0c7d12ae 100644 --- a/core/commonMain/src/kotlinx/serialization/modules/SerializersModuleCollector.kt +++ b/core/commonMain/src/kotlinx/serialization/modules/SerializersModuleCollector.kt @@ -59,20 +59,6 @@ public interface SerializersModuleCollector { defaultSerializerProvider: (value: Base) -> SerializationStrategy? ) - /* - // TODO remove - @Deprecated( - "Deprecated in favor of function with new `PolymorphicDeserializerProvider` API", - level = DeprecationLevel.WARNING // Since TODO. Raise to ERROR in TODO, hide in TODO - ) - public fun polymorphicDefaultDeserializer( - baseClass: KClass, - defaultDeserializerProvider: (className: String?) -> DeserializationStrategy? - ) { - polymorphicDefaultDeserializer(baseClass, defaultDeserializerProvider.toNewApi()) - } - */ - /** * Accept a default deserializer provider, associated with the [baseClass] for polymorphic deserialization. * [defaultDeserializerProvider] is invoked when no polymorphic serializers associated with the `className` From f80d7a8471ae422f83dba956d0fdca614867d316 Mon Sep 17 00:00:00 2001 From: Shreck Ye Date: Sun, 18 Feb 2024 12:37:37 +0800 Subject: [PATCH 03/12] Remove unused code related to storing the map from polymorphic subclasses to polymorphic numbers See commit 3289fb22b30c7fe5ec7d577e3ffed3b0bc8b0a71. --- .../internal/AbstractPolymorphicSerializer.kt | 1 - .../kotlinx/serialization/modules/SerializersModule.kt | 10 ---------- 2 files changed, 11 deletions(-) diff --git a/core/commonMain/src/kotlinx/serialization/internal/AbstractPolymorphicSerializer.kt b/core/commonMain/src/kotlinx/serialization/internal/AbstractPolymorphicSerializer.kt index 7b0c3bfe1a..209fc970df 100644 --- a/core/commonMain/src/kotlinx/serialization/internal/AbstractPolymorphicSerializer.kt +++ b/core/commonMain/src/kotlinx/serialization/internal/AbstractPolymorphicSerializer.kt @@ -36,7 +36,6 @@ public abstract class AbstractPolymorphicSerializer internal constructo encodeIntElement( descriptor, 0, - // it seems not possible to cache this with the current implementation that serializers are completely separated from serializers modules actualSerializer.descriptor.serialPolymorphicNumberByBaseClass.getValue(baseClass) ) else diff --git a/core/commonMain/src/kotlinx/serialization/modules/SerializersModule.kt b/core/commonMain/src/kotlinx/serialization/modules/SerializersModule.kt index 956dadf8a3..1aee35c1a2 100644 --- a/core/commonMain/src/kotlinx/serialization/modules/SerializersModule.kt +++ b/core/commonMain/src/kotlinx/serialization/modules/SerializersModule.kt @@ -69,11 +69,6 @@ public sealed class SerializersModule { baseClass: KClass, serializedNumber: Int? ): DeserializationStrategy? - // TODO remove or use - // TODO old design for cashing serializers by number in `AbstractPolymorphicSerializer` which probably doesn't work and is not needed - @ExperimentalSerializationApi - public abstract fun getPolymorphicForAllSubclasses(baseClass: KClass): Map, KSerializer> - /** * Copies contents of this module to the given [collector]. */ @@ -203,11 +198,6 @@ internal class SerialModuleImpl( ) } - // TODO remove or use - @ExperimentalSerializationApi - override fun getPolymorphicForAllSubclasses(baseClass: KClass): Map, KSerializer> = - polyBase2Serializers.getValue(baseClass) as Map, KSerializer> - override fun getContextual(kClass: KClass, typeArgumentsSerializers: List>): KSerializer? { return (class2ContextualFactory[kClass]?.invoke(typeArgumentsSerializers)) as? KSerializer? } From 5834dcd0d696b4f1f0efcf5045e8857c7378d609 Mon Sep 17 00:00:00 2001 From: Shreck Ye Date: Tue, 20 Feb 2024 21:52:12 +0800 Subject: [PATCH 04/12] Locate and fix a bug caused by lazy evaluation brought early in `SealedSerializer` that causes the tests to break --- .../commonMain/src/kotlinx/serialization/SealedSerializer.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/core/commonMain/src/kotlinx/serialization/SealedSerializer.kt b/core/commonMain/src/kotlinx/serialization/SealedSerializer.kt index eb6f29cf54..286338a189 100644 --- a/core/commonMain/src/kotlinx/serialization/SealedSerializer.kt +++ b/core/commonMain/src/kotlinx/serialization/SealedSerializer.kt @@ -117,7 +117,6 @@ public class SealedClassSerializer( private val class2Serializer: Map, KSerializer> private val serialName2Serializer: Map> - private val serialPolymorphicNumber2Serializer : Map>? init { if (subclasses.size != subclassSerializers.size) { @@ -140,8 +139,10 @@ public class SealedClassSerializer( } element }.mapValues { it.value.value } + } - serialPolymorphicNumber2Serializer = if (descriptor.useSerialPolymorphicNumbers) + private val serialPolymorphicNumber2Serializer: Map>? by lazy(LazyThreadSafetyMode.PUBLICATION) { + if (descriptor.useSerialPolymorphicNumbers) class2Serializer.entries.groupingBy { it.value.descriptor.serialPolymorphicNumberByBaseClass.getValue(baseClass) } From 5c24ef01ae8dff9a08502149301e4489d6c41c59 Mon Sep 17 00:00:00 2001 From: Shreck Ye Date: Tue, 20 Feb 2024 21:58:42 +0800 Subject: [PATCH 05/12] Remove a redundant suppression annotation --- .../src/kotlinx/serialization/descriptors/SerialDescriptors.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/core/commonMain/src/kotlinx/serialization/descriptors/SerialDescriptors.kt b/core/commonMain/src/kotlinx/serialization/descriptors/SerialDescriptors.kt index df5eb3c6c8..88200ca2c5 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, From 51d9a75f55424324fe38f7a3441aab2907e85e8f Mon Sep 17 00:00:00 2001 From: Shreck Ye Date: Sat, 24 Feb 2024 18:45:49 +0800 Subject: [PATCH 06/12] Add some tests for serial polymorphic numbers which fail for now Some miscellaneous changes: 1. Add a `getSerialPolymorphicNumberByBaseClass` function in `SerialDescriptor` to throw the appropriate exception. 1. Add `defaultDeserializerForNumber` which was missing in `PolymorphicModuleBuilder`. --- .../kotlinx/serialization/SealedSerializer.kt | 2 +- .../descriptors/SerialDescriptor.kt | 7 ++ .../internal/AbstractPolymorphicSerializer.kt | 4 +- .../modules/PolymorphicModuleBuilder.kt | 15 ++++ .../modules/SerializersModuleBuilders.kt | 4 +- .../SerialPolymorphicNumberTest.kt | 74 +++++++++++++++++++ .../serialization/test/JsonTestHelpers.kt | 18 +++++ 7 files changed, 118 insertions(+), 6 deletions(-) create mode 100644 formats/json-tests/commonTest/src/kotlinx/serialization/SerialPolymorphicNumberTest.kt create mode 100644 formats/json-tests/commonTest/src/kotlinx/serialization/test/JsonTestHelpers.kt diff --git a/core/commonMain/src/kotlinx/serialization/SealedSerializer.kt b/core/commonMain/src/kotlinx/serialization/SealedSerializer.kt index 286338a189..82452d866e 100644 --- a/core/commonMain/src/kotlinx/serialization/SealedSerializer.kt +++ b/core/commonMain/src/kotlinx/serialization/SealedSerializer.kt @@ -144,7 +144,7 @@ public class SealedClassSerializer( private val serialPolymorphicNumber2Serializer: Map>? by lazy(LazyThreadSafetyMode.PUBLICATION) { if (descriptor.useSerialPolymorphicNumbers) class2Serializer.entries.groupingBy { - it.value.descriptor.serialPolymorphicNumberByBaseClass.getValue(baseClass) + it.value.descriptor.getSerialPolymorphicNumberByBaseClass(baseClass) } .aggregate, KSerializer>, Int, Map.Entry, KSerializer>> { key, accumulator, element, _ -> diff --git a/core/commonMain/src/kotlinx/serialization/descriptors/SerialDescriptor.kt b/core/commonMain/src/kotlinx/serialization/descriptors/SerialDescriptor.kt index 94edbce928..28f11f1012 100644 --- a/core/commonMain/src/kotlinx/serialization/descriptors/SerialDescriptor.kt +++ b/core/commonMain/src/kotlinx/serialization/descriptors/SerialDescriptor.kt @@ -208,6 +208,13 @@ public interface SerialDescriptor { @ExperimentalSerializationApi public val serialPolymorphicNumberByBaseClass: Map, Int> get() = emptyMap() + @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 ${baseClass.simpleName}.") + } + /** * Returns serial annotations of the associated class. * Serial annotations can be used to specify an additional metadata that may be used during serialization. diff --git a/core/commonMain/src/kotlinx/serialization/internal/AbstractPolymorphicSerializer.kt b/core/commonMain/src/kotlinx/serialization/internal/AbstractPolymorphicSerializer.kt index 209fc970df..8940b03e20 100644 --- a/core/commonMain/src/kotlinx/serialization/internal/AbstractPolymorphicSerializer.kt +++ b/core/commonMain/src/kotlinx/serialization/internal/AbstractPolymorphicSerializer.kt @@ -34,9 +34,7 @@ public abstract class AbstractPolymorphicSerializer internal constructo encoder.encodeStructure(descriptor) { if (descriptor.useSerialPolymorphicNumbers) encodeIntElement( - descriptor, - 0, - actualSerializer.descriptor.serialPolymorphicNumberByBaseClass.getValue(baseClass) + descriptor, 0, actualSerializer.descriptor.getSerialPolymorphicNumberByBaseClass(baseClass) ) else encodeStringElement(descriptor, 0, actualSerializer.descriptor.serialName) diff --git a/core/commonMain/src/kotlinx/serialization/modules/PolymorphicModuleBuilder.kt b/core/commonMain/src/kotlinx/serialization/modules/PolymorphicModuleBuilder.kt index 1b1c4e01c5..798842f152 100644 --- a/core/commonMain/src/kotlinx/serialization/modules/PolymorphicModuleBuilder.kt +++ b/core/commonMain/src/kotlinx/serialization/modules/PolymorphicModuleBuilder.kt @@ -22,6 +22,7 @@ public class PolymorphicModuleBuilder @PublishedApi internal cons private val subclasses: MutableList, KSerializer>> = mutableListOf() private var defaultSerializerProvider: ((Base) -> SerializationStrategy?)? = null private var defaultDeserializerProvider: PolymorphicDeserializerProvider? = null + private var defaultDeserializerProviderForNumber: PolymorphicDeserializerProviderForNumber? = null /* // TODO implement this or remove? @@ -75,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]. @@ -123,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/SerializersModuleBuilders.kt b/core/commonMain/src/kotlinx/serialization/modules/SerializersModuleBuilders.kt index 2503b5c24b..e66f11d2e6 100644 --- a/core/commonMain/src/kotlinx/serialization/modules/SerializersModuleBuilders.kt +++ b/core/commonMain/src/kotlinx/serialization/modules/SerializersModuleBuilders.kt @@ -230,7 +230,7 @@ public class SerializersModuleBuilder @PublishedApi internal constructor() : Ser // Remove previous serializers from name mapping if (previousSerializer != null) { names.remove(previousSerializer.descriptor.serialName) - numbers.remove(previousSerializer.descriptor.serialPolymorphicNumberByBaseClass[baseClass]) + previousSerializer.descriptor.serialPolymorphicNumberByBaseClass[baseClass]?.let { numbers.remove(it) } } // Update mappings baseClassSerializers[concreteClass] = concreteSerializer @@ -245,7 +245,7 @@ public class SerializersModuleBuilder @PublishedApi internal constructor() : Ser } else { // Cleanup name mapping names.remove(previousSerializer.descriptor.serialName) - numbers.remove(previousSerializer.descriptor.serialPolymorphicNumberByBaseClass[baseClass]) + previousSerializer.descriptor.serialPolymorphicNumberByBaseClass[baseClass]?.let { numbers.remove(it) } } } val previousByName = names[name] 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..1c52aca4c8 --- /dev/null +++ b/formats/json-tests/commonTest/src/kotlinx/serialization/SerialPolymorphicNumberTest.kt @@ -0,0 +1,74 @@ +/* + * 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) + class Case : Sealed1() + } + + @Serializable + sealed class Sealed2 { + @Serializable + @SerialPolymorphicNumber(Sealed2::class, 1) + class Case : Sealed2() + } + + @Serializable + @UseSerialPolymorphicNumbers + sealed class Sealed3 { + @Serializable + class Case : Sealed3() + } + + @Test + fun testSealed() { + testConversion(Sealed1.Case(), """{"type":1}""") + testConversion(Sealed2.Case(), """{"type":"kotlinx.serialization.SerialPolymorphicNumberTest.Case"}""") + assertFailsWith(SerializationException::class) { + Json.encodeToString(Sealed3.Case()) + } + assertFailsWith(SerializationException::class) { + Json.decodeFromString("{}") + } + } + + @Serializable + @UseSerialPolymorphicNumbers + sealed class Abstract { + @Serializable + @SerialPolymorphicNumber(Abstract::class, 1) + class Case : Abstract() + + @Serializable + 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}""") + testConversion(json, Abstract.Default(0), """{"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..0866e60d06 --- /dev/null +++ b/formats/json-tests/commonTest/src/kotlinx/serialization/test/JsonTestHelpers.kt @@ -0,0 +1,18 @@ +/* + * 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, expectedHexString: String) { + val string = json.encodeToString(data) + assertEquals(expectedHexString, string) + assertEquals(data, json.decodeFromString(string)) +} + +inline fun testConversion(data: T, expectedHexString: String) = + testConversion(Json, data, expectedHexString) From e101ae8e7805bfb6659a32394b2924da80afe243 Mon Sep 17 00:00:00 2001 From: Shreck Ye Date: Tue, 27 Feb 2024 16:28:52 +0800 Subject: [PATCH 07/12] Enable Kotlin compiler bootstrap, use the corresponding SNAPSHOT version, and update an annotation property name The corresponding commit: https://github.com/huanshankeji/kotlin/commit/9860724c856e31b044321b21b59a5f8e87a3de70 --- core/commonMain/src/kotlinx/serialization/Annotations.kt | 2 +- gradle.properties | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/core/commonMain/src/kotlinx/serialization/Annotations.kt b/core/commonMain/src/kotlinx/serialization/Annotations.kt index 20f7b962e3..f8abe7959e 100644 --- a/core/commonMain/src/kotlinx/serialization/Annotations.kt +++ b/core/commonMain/src/kotlinx/serialization/Annotations.kt @@ -171,7 +171,7 @@ public annotation class UseSerialPolymorphicNumbers @MustBeDocumented @Target(AnnotationTarget.CLASS) @Repeatable -public annotation class SerialPolymorphicNumber(val baseClass: KClass<*>, val value: Int) +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/gradle.properties b/gradle.properties index ff245336b3..94756b50eb 100644 --- a/gradle.properties +++ b/gradle.properties @@ -7,8 +7,9 @@ version=1.6.3-SNAPSHOT kotlin.version=1.9.21 +bootstrap=true # This version takes precedence if 'bootstrap' property passed to project -kotlin.version.snapshot=1.9.255-SNAPSHOT +kotlin.version.snapshot=1.9.255-serialization-plugin-polymorphic-number-SNAPSHOT # Also set KONAN_LOCAL_DIST environment variable in bootstrap mode to auto-assign konan.home junit_version=4.12 From 1aa1354eb469992221868849385b6dee047da544 Mon Sep 17 00:00:00 2001 From: Shreck Ye Date: Wed, 28 Feb 2024 18:05:29 +0800 Subject: [PATCH 08/12] Move `useSerialPolymorphicNumbers` and `serialPolymorphicNumberByBaseClass` into `PluginGeneratedSerialDescriptor` where it belongs and add some more tests in `SerialPolymorphicNumberTest` (which fail for now) --- .../descriptors/SerialDescriptors.kt | 18 +----------------- .../PluginGeneratedSerialDescriptor.kt | 5 ++++- .../SerialPolymorphicNumberTest.kt | 15 +++++++++++++++ 3 files changed, 20 insertions(+), 18 deletions(-) diff --git a/core/commonMain/src/kotlinx/serialization/descriptors/SerialDescriptors.kt b/core/commonMain/src/kotlinx/serialization/descriptors/SerialDescriptors.kt index 88200ca2c5..de3cc17375 100644 --- a/core/commonMain/src/kotlinx/serialization/descriptors/SerialDescriptors.kt +++ b/core/commonMain/src/kotlinx/serialization/descriptors/SerialDescriptors.kt @@ -53,8 +53,6 @@ import kotlin.reflect.* public fun buildClassSerialDescriptor( serialName: String, vararg typeParameters: SerialDescriptor, - useSerialPolymorphicNumbers: Boolean = false, - serialPolymorphicNumbers: Map, Int> = emptyMap(), builderAction: ClassSerialDescriptorBuilder.() -> Unit = {} ): SerialDescriptor { require(serialName.isNotBlank()) { "Blank serial names are prohibited" } @@ -65,8 +63,6 @@ public fun buildClassSerialDescriptor( StructureKind.CLASS, sdBuilder.elementNames.size, typeParameters.toList(), - useSerialPolymorphicNumbers, - serialPolymorphicNumbers, sdBuilder ) } @@ -143,23 +139,13 @@ public fun buildSerialDescriptor( serialName: String, kind: SerialKind, vararg typeParameters: SerialDescriptor, - useSerialPolymorphicNumbers: Boolean = false, - serialPolymorphicNumbers: Map, Int> = emptyMap(), builder: ClassSerialDescriptorBuilder.() -> Unit = {} ): SerialDescriptor { require(serialName.isNotBlank()) { "Blank serial names are prohibited" } require(kind != StructureKind.CLASS) { "For StructureKind.CLASS please use 'buildClassSerialDescriptor' instead" } val sdBuilder = ClassSerialDescriptorBuilder(serialName) sdBuilder.builder() - return SerialDescriptorImpl( - serialName, - kind, - sdBuilder.elementNames.size, - typeParameters.toList(), - useSerialPolymorphicNumbers, - serialPolymorphicNumbers, - sdBuilder - ) + return SerialDescriptorImpl(serialName, kind, sdBuilder.elementNames.size, typeParameters.toList(), sdBuilder) } @@ -322,8 +308,6 @@ internal class SerialDescriptorImpl( override val kind: SerialKind, override val elementsCount: Int, typeParameters: List, - override val useSerialPolymorphicNumbers : Boolean, - override val serialPolymorphicNumberByBaseClass : Map, Int>, builder: ClassSerialDescriptorBuilder ) : SerialDescriptor, CachedNames { diff --git a/core/commonMain/src/kotlinx/serialization/internal/PluginGeneratedSerialDescriptor.kt b/core/commonMain/src/kotlinx/serialization/internal/PluginGeneratedSerialDescriptor.kt index a954bdab00..266cd3217d 100644 --- a/core/commonMain/src/kotlinx/serialization/internal/PluginGeneratedSerialDescriptor.kt +++ b/core/commonMain/src/kotlinx/serialization/internal/PluginGeneratedSerialDescriptor.kt @@ -8,6 +8,7 @@ package kotlinx.serialization.internal import kotlinx.serialization.* import kotlinx.serialization.descriptors.* import kotlinx.serialization.encoding.CompositeDecoder.Companion.UNKNOWN_NAME +import kotlin.reflect.* /** * Implementation that plugin uses to implement descriptors for auto-generated serializers. @@ -17,7 +18,9 @@ import kotlinx.serialization.encoding.CompositeDecoder.Companion.UNKNOWN_NAME internal open class PluginGeneratedSerialDescriptor( override val serialName: String, private val generatedSerializer: GeneratedSerializer<*>? = null, - final override val elementsCount: Int + final override val elementsCount: Int, + override val useSerialPolymorphicNumbers: Boolean = false, + override val serialPolymorphicNumberByBaseClass: Map, Int> = emptyMap() ) : SerialDescriptor, CachedNames { override val kind: SerialKind get() = StructureKind.CLASS override val annotations: List get() = classAnnotations ?: emptyList() diff --git a/formats/json-tests/commonTest/src/kotlinx/serialization/SerialPolymorphicNumberTest.kt b/formats/json-tests/commonTest/src/kotlinx/serialization/SerialPolymorphicNumberTest.kt index 1c52aca4c8..f60b2810ee 100644 --- a/formats/json-tests/commonTest/src/kotlinx/serialization/SerialPolymorphicNumberTest.kt +++ b/formats/json-tests/commonTest/src/kotlinx/serialization/SerialPolymorphicNumberTest.kt @@ -32,6 +32,19 @@ class SerialPolymorphicNumberTest { class Case : Sealed3() } + @Serializable + @UseSerialPolymorphicNumbers + sealed class Sealed4 { + @Serializable + @UseSerialPolymorphicNumbers + sealed class Sealed41 : Sealed4(){ + @Serializable + @SerialPolymorphicNumber(Sealed4::class, 1) + @SerialPolymorphicNumber(Sealed41::class, 2) + class Case : Sealed41() + } + } + @Test fun testSealed() { testConversion(Sealed1.Case(), """{"type":1}""") @@ -42,6 +55,8 @@ class SerialPolymorphicNumberTest { assertFailsWith(SerializationException::class) { Json.decodeFromString("{}") } + testConversion(Sealed4.Sealed41.Case(), """{"type":1}""") + testConversion(Sealed4.Sealed41.Case(), """{"type":2}""") } @Serializable From bcae134ff9b5d5e2778f05c57d64f11853a68cb9 Mon Sep 17 00:00:00 2001 From: Shreck Ye Date: Sat, 2 Mar 2024 19:04:03 +0800 Subject: [PATCH 09/12] Mark the annotations `UseSerialPolymorphicNumbers` and `SerialPolymorphicNumber` with `@SerialInfo` to be stored in `SerialDescriptor.annotations` and refactor related code Main changes: 1. Extract common lazy properties in `CommonSerialDescriptor` and make both `PluginGeneratedSerialDescriptor` and `SerialDescriptorImpl` inherit it. 1. Support serial polymorphic numbers with JSON's custom implementations in `AbstractJsonTreeEncoder`, `StreamingJsonEncoder`, and `DynamicObjectEncoder` while adapting the `encodePolymorphically` and `decodeSerializableValuePolymorphic` functions. 1. Revert gradle.properties since there is no need to update the compiler plugin anymore. 1. Make all tests pass in `SerialPolymorphicNumberTest` and copy it into the Protobuf module and adapt. --- .../src/kotlinx/serialization/Annotations.kt | 9 +- .../descriptors/SerialDescriptor.kt | 35 ++++-- .../descriptors/SerialDescriptors.kt | 2 +- .../internal/CommonSerialDescriptor.kt | 17 +++ .../PluginGeneratedSerialDescriptor.kt | 7 +- .../SerialPolymorphicNumberTest.kt | 35 +++--- .../serialization/test/JsonTestHelpers.kt | 20 +++- .../json/internal/Polymorphic.kt | 22 +++- .../json/internal/StreamingJsonEncoder.kt | 11 +- .../json/internal/TreeJsonEncoder.kt | 13 +- .../json/internal/DynamicEncoders.kt | 10 +- .../protobuf/SerialPolymorphicNumberTest.kt | 112 ++++++++++++++++++ gradle.properties | 3 +- 13 files changed, 234 insertions(+), 62 deletions(-) create mode 100644 core/commonMain/src/kotlinx/serialization/internal/CommonSerialDescriptor.kt create mode 100644 formats/protobuf/commonTest/src/kotlinx/serialization/protobuf/SerialPolymorphicNumberTest.kt diff --git a/core/commonMain/src/kotlinx/serialization/Annotations.kt b/core/commonMain/src/kotlinx/serialization/Annotations.kt index f8abe7959e..65fabaadc4 100644 --- a/core/commonMain/src/kotlinx/serialization/Annotations.kt +++ b/core/commonMain/src/kotlinx/serialization/Annotations.kt @@ -155,22 +155,23 @@ public annotation class SerialName(val value: String) /** * Requires all subclasses to use [SerialPolymorphicNumber]. */ -@MustBeDocumented +@SerialInfo @Target(AnnotationTarget.CLASS) -@Repeatable +@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 [value]. + * 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. */ -@MustBeDocumented +@SerialInfo @Target(AnnotationTarget.CLASS) @Repeatable +@ExperimentalSerializationApi public annotation class SerialPolymorphicNumber(val baseClass: KClass<*>, val number: Int) /** diff --git a/core/commonMain/src/kotlinx/serialization/descriptors/SerialDescriptor.kt b/core/commonMain/src/kotlinx/serialization/descriptors/SerialDescriptor.kt index 28f11f1012..b735a6219a 100644 --- a/core/commonMain/src/kotlinx/serialization/descriptors/SerialDescriptor.kt +++ b/core/commonMain/src/kotlinx/serialization/descriptors/SerialDescriptor.kt @@ -196,33 +196,44 @@ public interface SerialDescriptor { @ExperimentalSerializationApi public val elementsCount: Int + /** + * Returns serial annotations of the associated class. + * Serial annotations can be used to specify an additional metadata that may be used during serialization. + * Only annotations marked with [SerialInfo] are added to the resulting list. + */ + @ExperimentalSerializationApi + public val annotations: List get() = emptyList() + /** * TODO */ @ExperimentalSerializationApi - public val useSerialPolymorphicNumbers: Boolean get() = false + public val useSerialPolymorphicNumbers: Boolean + get() = + annotations.any { it is UseSerialPolymorphicNumbers } /** * TODO */ @ExperimentalSerializationApi - public val serialPolymorphicNumberByBaseClass: Map, Int> get() = emptyMap() + 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 ${baseClass.simpleName}.") + 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 serial annotations of the associated class. - * Serial annotations can be used to specify an additional metadata that may be used during serialization. - * Only annotations marked with [SerialInfo] are added to the resulting list. - */ - @ExperimentalSerializationApi - public val annotations: List get() = emptyList() - /** * 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 de3cc17375..6fe2e39a8e 100644 --- a/core/commonMain/src/kotlinx/serialization/descriptors/SerialDescriptors.kt +++ b/core/commonMain/src/kotlinx/serialization/descriptors/SerialDescriptors.kt @@ -309,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/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 266cd3217d..0053097907 100644 --- a/core/commonMain/src/kotlinx/serialization/internal/PluginGeneratedSerialDescriptor.kt +++ b/core/commonMain/src/kotlinx/serialization/internal/PluginGeneratedSerialDescriptor.kt @@ -8,7 +8,6 @@ package kotlinx.serialization.internal import kotlinx.serialization.* import kotlinx.serialization.descriptors.* import kotlinx.serialization.encoding.CompositeDecoder.Companion.UNKNOWN_NAME -import kotlin.reflect.* /** * Implementation that plugin uses to implement descriptors for auto-generated serializers. @@ -18,10 +17,8 @@ import kotlin.reflect.* internal open class PluginGeneratedSerialDescriptor( override val serialName: String, private val generatedSerializer: GeneratedSerializer<*>? = null, - final override val elementsCount: Int, - override val useSerialPolymorphicNumbers: Boolean = false, - override val serialPolymorphicNumberByBaseClass: Map, Int> = emptyMap() -) : SerialDescriptor, CachedNames { + final override val elementsCount: Int +) : CommonSerialDescriptor(), CachedNames { override val kind: SerialKind get() = StructureKind.CLASS override val annotations: List get() = classAnnotations ?: emptyList() diff --git a/formats/json-tests/commonTest/src/kotlinx/serialization/SerialPolymorphicNumberTest.kt b/formats/json-tests/commonTest/src/kotlinx/serialization/SerialPolymorphicNumberTest.kt index f60b2810ee..5595a6cf65 100644 --- a/formats/json-tests/commonTest/src/kotlinx/serialization/SerialPolymorphicNumberTest.kt +++ b/formats/json-tests/commonTest/src/kotlinx/serialization/SerialPolymorphicNumberTest.kt @@ -15,21 +15,25 @@ class SerialPolymorphicNumberTest { sealed class Sealed1 { @Serializable @SerialPolymorphicNumber(Sealed1::class, 1) - class Case : Sealed1() + data class Case1(val property: Int) : Sealed1() + + @Serializable + @SerialPolymorphicNumber(Sealed1::class, 2) + object Case2 : Sealed1() } @Serializable sealed class Sealed2 { @Serializable @SerialPolymorphicNumber(Sealed2::class, 1) - class Case : Sealed2() + object Case : Sealed2() } @Serializable @UseSerialPolymorphicNumbers sealed class Sealed3 { @Serializable - class Case : Sealed3() + object Case : Sealed3() } @Serializable @@ -37,37 +41,38 @@ class SerialPolymorphicNumberTest { sealed class Sealed4 { @Serializable @UseSerialPolymorphicNumbers - sealed class Sealed41 : Sealed4(){ + sealed class Sealed41 : Sealed4() { @Serializable @SerialPolymorphicNumber(Sealed4::class, 1) @SerialPolymorphicNumber(Sealed41::class, 2) - class Case : Sealed41() + object Case : Sealed41() } } @Test fun testSealed() { - testConversion(Sealed1.Case(), """{"type":1}""") - testConversion(Sealed2.Case(), """{"type":"kotlinx.serialization.SerialPolymorphicNumberTest.Case"}""") + 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()) + Json.encodeToString(Sealed3.Case) } assertFailsWith(SerializationException::class) { Json.decodeFromString("{}") } - testConversion(Sealed4.Sealed41.Case(), """{"type":1}""") - testConversion(Sealed4.Sealed41.Case(), """{"type":2}""") + testConversion(Sealed4.Sealed41.Case, """{"type":1}""") + testConversion(Sealed4.Sealed41.Case, """{"type":2}""") } @Serializable @UseSerialPolymorphicNumbers - sealed class Abstract { + abstract class Abstract { @Serializable @SerialPolymorphicNumber(Abstract::class, 1) - class Case : Abstract() + object Case : Abstract() @Serializable - class Default(val type: Int?):Abstract() + data class Default(val type: Int?) : Abstract() } val json = Json { @@ -83,7 +88,7 @@ class SerialPolymorphicNumberTest { @Test fun testPolymorphicModule() { - testConversion(json, Abstract.Case(), """{"type":1}""") - testConversion(json, Abstract.Default(0), """{"type":0}""") + 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 index 0866e60d06..c6274036b7 100644 --- a/formats/json-tests/commonTest/src/kotlinx/serialization/test/JsonTestHelpers.kt +++ b/formats/json-tests/commonTest/src/kotlinx/serialization/test/JsonTestHelpers.kt @@ -8,11 +8,19 @@ import kotlinx.serialization.* import kotlinx.serialization.json.* import kotlin.test.* -inline fun testConversion(json: Json, data: T, expectedHexString: String) { - val string = json.encodeToString(data) - assertEquals(expectedHexString, string) - assertEquals(data, json.decodeFromString(string)) +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, expectedHexString: String) = - testConversion(Json, data, expectedHexString) +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/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..ba4e9822f5 --- /dev/null +++ b/formats/protobuf/commonTest/src/kotlinx/serialization/protobuf/SerialPolymorphicNumberTest.kt @@ -0,0 +1,112 @@ +/* + * 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 + data class Default(val type: Int?) : 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") + // TODO Not working. However, even the original default serializer for serial names is not working for Protobuf as tested. + // assertEquals(Abstract.Default(0), protoBuf.decodeFromHexString("08001200")) + } +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index 94756b50eb..ff245336b3 100644 --- a/gradle.properties +++ b/gradle.properties @@ -7,9 +7,8 @@ version=1.6.3-SNAPSHOT kotlin.version=1.9.21 -bootstrap=true # This version takes precedence if 'bootstrap' property passed to project -kotlin.version.snapshot=1.9.255-serialization-plugin-polymorphic-number-SNAPSHOT +kotlin.version.snapshot=1.9.255-SNAPSHOT # Also set KONAN_LOCAL_DIST environment variable in bootstrap mode to auto-assign konan.home junit_version=4.12 From c53d324c1871c2f69a786ddb3cbd4a5edcfd0782 Mon Sep 17 00:00:00 2001 From: Shreck Ye Date: Sat, 2 Mar 2024 19:14:03 +0800 Subject: [PATCH 10/12] Run `:apiDump` and review --- core/api/kotlinx-serialization-core.api | 39 +++++++++++++++++++++++-- 1 file changed, 36 insertions(+), 3 deletions(-) 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 } From 4973f071d84d8119008fc8facf3db43858305d0b Mon Sep 17 00:00:00 2001 From: Shreck Ye Date: Sat, 2 Mar 2024 22:52:55 +0800 Subject: [PATCH 11/12] Review all changes since commit 41c0bb10bcb04bb8c72828b0b5fe63ab85a348f5, improving some code and comments and reverting some unnecessary changes --- core/commonMain/src/kotlinx/serialization/Annotations.kt | 2 +- .../src/kotlinx/serialization/SealedSerializer.kt | 1 - .../serialization/internal/AbstractPolymorphicSerializer.kt | 6 +++--- .../serialization/modules/SerializersModuleBuilders.kt | 4 ++-- 4 files changed, 6 insertions(+), 7 deletions(-) diff --git a/core/commonMain/src/kotlinx/serialization/Annotations.kt b/core/commonMain/src/kotlinx/serialization/Annotations.kt index 65fabaadc4..cf99594f11 100644 --- a/core/commonMain/src/kotlinx/serialization/Annotations.kt +++ b/core/commonMain/src/kotlinx/serialization/Annotations.kt @@ -153,7 +153,7 @@ public annotation class Serializer( public annotation class SerialName(val value: String) /** - * Requires all subclasses to use [SerialPolymorphicNumber]. + * Requires all subclasses marked with this annotation to use [SerialPolymorphicNumber]. */ @SerialInfo @Target(AnnotationTarget.CLASS) diff --git a/core/commonMain/src/kotlinx/serialization/SealedSerializer.kt b/core/commonMain/src/kotlinx/serialization/SealedSerializer.kt index 82452d866e..ccb14c1dba 100644 --- a/core/commonMain/src/kotlinx/serialization/SealedSerializer.kt +++ b/core/commonMain/src/kotlinx/serialization/SealedSerializer.kt @@ -127,7 +127,6 @@ public class SealedClassSerializer( // Plugin should produce identical serializers, although they are not always strictly equal (e.g. new ObjectSerializer // may be created every time) class2Serializer = subclasses.zip(subclassSerializers).toMap() - serialName2Serializer = class2Serializer.entries.groupingBy { it.value.descriptor.serialName } .aggregate, KSerializer>, String, Map.Entry, KSerializer>> { key, accumulator, element, _ -> diff --git a/core/commonMain/src/kotlinx/serialization/internal/AbstractPolymorphicSerializer.kt b/core/commonMain/src/kotlinx/serialization/internal/AbstractPolymorphicSerializer.kt index 8940b03e20..bb7f0b2f1f 100644 --- a/core/commonMain/src/kotlinx/serialization/internal/AbstractPolymorphicSerializer.kt +++ b/core/commonMain/src/kotlinx/serialization/internal/AbstractPolymorphicSerializer.kt @@ -143,11 +143,11 @@ internal fun throwSubtypeNotRegistered(serialPolymorphicNumber: Int?, baseClass: throw SerializationException( ( if (serialPolymorphicNumber == null) - "Class discriminator serial polymorphic number was missing and no default serializers were registered $scope." + "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 $scope.\n" + + "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}' has to be sealed and '@Serializable'.\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." ) diff --git a/core/commonMain/src/kotlinx/serialization/modules/SerializersModuleBuilders.kt b/core/commonMain/src/kotlinx/serialization/modules/SerializersModuleBuilders.kt index e66f11d2e6..f7026ee447 100644 --- a/core/commonMain/src/kotlinx/serialization/modules/SerializersModuleBuilders.kt +++ b/core/commonMain/src/kotlinx/serialization/modules/SerializersModuleBuilders.kt @@ -188,7 +188,7 @@ public class SerializersModuleBuilder @PublishedApi internal constructor() : Ser internal fun registerDefaultPolymorphicDeserializer( baseClass: KClass, defaultDeserializerProvider: (className: String?) -> DeserializationStrategy?, - //defaultDeserializerProvider: PolymorphicDeserializerProvider, + //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] @@ -202,7 +202,7 @@ public class SerializersModuleBuilder @PublishedApi internal constructor() : Ser internal fun registerDefaultPolymorphicDeserializerForNumber( baseClass: KClass, defaultDeserializerProvider: (polymorphicSerialNumber: Int?) -> DeserializationStrategy?, - //defaultDeserializerProvider: PolymorphicDeserializerProviderForNumber, + //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] From 23c385cc334f0224666d6d2783b2b938f8991d07 Mon Sep 17 00:00:00 2001 From: Shreck Ye Date: Wed, 13 Mar 2024 00:22:28 +0800 Subject: [PATCH 12/12] Clarify the misconception in the test for `defaultDeserializerForNumber` and make it work See #2598. --- .../serialization/protobuf/SerialPolymorphicNumberTest.kt | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/formats/protobuf/commonTest/src/kotlinx/serialization/protobuf/SerialPolymorphicNumberTest.kt b/formats/protobuf/commonTest/src/kotlinx/serialization/protobuf/SerialPolymorphicNumberTest.kt index ba4e9822f5..a014dd7f73 100644 --- a/formats/protobuf/commonTest/src/kotlinx/serialization/protobuf/SerialPolymorphicNumberTest.kt +++ b/formats/protobuf/commonTest/src/kotlinx/serialization/protobuf/SerialPolymorphicNumberTest.kt @@ -89,7 +89,7 @@ class SerialPolymorphicNumberTest { object Case : Abstract() @Serializable - data class Default(val type: Int?) : Abstract() + object Default : Abstract() } val protoBuf = ProtoBuf { @@ -106,7 +106,6 @@ class SerialPolymorphicNumberTest { @Test fun testPolymorphicModule() { testConversion(protoBuf, Abstract.Case, "08011200") - // TODO Not working. However, even the original default serializer for serial names is not working for Protobuf as tested. - // assertEquals(Abstract.Default(0), protoBuf.decodeFromHexString("08001200")) + assertEquals(Abstract.Default, protoBuf.decodeFromHexString("08001200")) } } \ No newline at end of file