From 2e960d0c35b26ea0f5b3dc460e7a32d34d97aa8e Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Tue, 18 Jan 2022 11:39:00 +0300 Subject: [PATCH] Best-effort kotlin reflect avoidance in serializer(Type) Fixes #1819 --- .../src/kotlinx/serialization/Annotations.kt | 2 + .../src/kotlinx/serialization/Serializers.kt | 7 ++- .../kotlinx/serialization/SerializersJvm.kt | 25 ++++++---- .../serialization/internal/Platform.kt | 49 +++++++++++-------- .../features/SerializerByTypeTest.kt | 13 ++++- 5 files changed, 60 insertions(+), 36 deletions(-) diff --git a/core/commonMain/src/kotlinx/serialization/Annotations.kt b/core/commonMain/src/kotlinx/serialization/Annotations.kt index b7710a90e..196300755 100644 --- a/core/commonMain/src/kotlinx/serialization/Annotations.kt +++ b/core/commonMain/src/kotlinx/serialization/Annotations.kt @@ -294,6 +294,7 @@ public annotation class Polymorphic * * Implementing [SerialDescriptor] interfaces * * Not-yet-stable serialization formats that require additional polishing */ +@MustBeDocumented @Target(AnnotationTarget.CLASS, AnnotationTarget.PROPERTY, AnnotationTarget.FUNCTION, AnnotationTarget.TYPEALIAS) @RequiresOptIn(level = RequiresOptIn.Level.WARNING) public annotation class ExperimentalSerializationApi @@ -305,6 +306,7 @@ public annotation class ExperimentalSerializationApi * and will be changed without any warnings or migration aids. * If you cannot avoid using internal API to solve your problem, please report your use-case to serialization's issue tracker. */ +@MustBeDocumented @Target(AnnotationTarget.CLASS, AnnotationTarget.PROPERTY, AnnotationTarget.FUNCTION, AnnotationTarget.TYPEALIAS) @RequiresOptIn(level = RequiresOptIn.Level.ERROR) public annotation class InternalSerializationApi diff --git a/core/commonMain/src/kotlinx/serialization/Serializers.kt b/core/commonMain/src/kotlinx/serialization/Serializers.kt index 97a00e6a2..a52118777 100644 --- a/core/commonMain/src/kotlinx/serialization/Serializers.kt +++ b/core/commonMain/src/kotlinx/serialization/Serializers.kt @@ -126,7 +126,6 @@ internal fun SerializersModule.reflectiveOrContextual(kClass: KClass SerializersModule.reflectiveOrContextual(kClass: KClass KClass.serializer(): KSerializer = serializerOrNull() * and it is not recommended to use this method for anything, but last-ditch resort, e.g. * when all type info is lost, your application has crashed and it is the final attempt to log or send some serializable data. * - * This API is not guaranteed to work consistent across different platforms or + * This API is not guaranteed to work consistently across different platforms or * to work in cases that slightly differ from "plain @Serializable class". * * ### Constraints - * This paragraph explains known (but not all!) constraints of the `serializer()` implementation. + * This paragraph explains known (but not all!) constraints of the `serializerOrNull()` implementation. * Please note that they are not bugs, but implementation restrictions that we cannot workaround. * * * This method may behave differently on JVM, JS and Native because of runtime reflection differences diff --git a/core/jvmMain/src/kotlinx/serialization/SerializersJvm.kt b/core/jvmMain/src/kotlinx/serialization/SerializersJvm.kt index 633836d35..b110f121b 100644 --- a/core/jvmMain/src/kotlinx/serialization/SerializersJvm.kt +++ b/core/jvmMain/src/kotlinx/serialization/SerializersJvm.kt @@ -56,7 +56,7 @@ public fun serializerOrNull(type: Type): KSerializer? = EmptySerializersMod */ @ExperimentalSerializationApi public fun SerializersModule.serializer(type: Type): KSerializer = - serializerByJavaTypeImpl(type, failOnMissingTypeArgSerializer = true) ?: type.kclass().serializerNotRegistered() + serializerByJavaTypeImpl(type, failOnMissingTypeArgSerializer = true) ?: type.prettyClass().serializerNotRegistered() /** * Retrieves serializer for the given reflective Java [type] using @@ -113,8 +113,7 @@ private fun SerializersModule.serializerByJavaTypeImpl(type: Type, failOnMissing // probably we should deprecate this method because it can't differ nullable vs non-nullable types // since it uses Java TypeToken, not Kotlin one val varargs = argsSerializers.map { it as KSerializer } - (rootClass.kotlin.constructSerializerForGivenTypeArgs(*(varargs.toTypedArray())) as? KSerializer) - ?: reflectiveOrContextual(rootClass.kotlin as KClass, varargs) + reflectiveOrContextual(rootClass as Class, varargs) } } } @@ -130,10 +129,17 @@ private fun SerializersModule.typeSerializer(type: Class<*>, failOnMissingTypeAr val arraySerializer = ArraySerializer(eType.kotlin as KClass, s) arraySerializer as KSerializer } else { - reflectiveOrContextual(type.kotlin as KClass, emptyList()) + reflectiveOrContextual(type as Class, emptyList()) } } +@OptIn(ExperimentalSerializationApi::class) +private fun SerializersModule.reflectiveOrContextual(jClass: Class, typeArgumentsSerializers: List>): KSerializer? { + jClass.constructSerializerForGivenTypeArgs(*typeArgumentsSerializers.toTypedArray())?.let { return it } + val kClass = jClass.kotlin + return kClass.builtinSerializerOrNull() ?: getContextual(kClass, typeArgumentsSerializers) +} + @OptIn(ExperimentalSerializationApi::class) private fun SerializersModule.genericArraySerializer( type: GenericArrayType, @@ -154,11 +160,10 @@ private fun SerializersModule.genericArraySerializer( return ArraySerializer(kclass, serializer) as KSerializer } -private fun Type.kclass(): KClass<*> = when (val it = this) { - is KClass<*> -> it - is Class<*> -> it.kotlin - is ParameterizedType -> it.rawType.kclass() - is WildcardType -> it.upperBounds.first().kclass() - is GenericArrayType -> it.genericComponentType.kclass() +private fun Type.prettyClass(): Class<*> = when (val it = this) { + is Class<*> -> it + is ParameterizedType -> it.rawType.prettyClass() + is WildcardType -> it.upperBounds.first().prettyClass() + is GenericArrayType -> it.genericComponentType.prettyClass() else -> throw IllegalArgumentException("typeToken should be an instance of Class, GenericArray, ParametrizedType or WildcardType, but actual type is $it ${it::class}") } diff --git a/core/jvmMain/src/kotlinx/serialization/internal/Platform.kt b/core/jvmMain/src/kotlinx/serialization/internal/Platform.kt index b4a966b8f..9dbb5a06d 100644 --- a/core/jvmMain/src/kotlinx/serialization/internal/Platform.kt +++ b/core/jvmMain/src/kotlinx/serialization/internal/Platform.kt @@ -28,24 +28,34 @@ internal actual fun ArrayList.toNativeArrayImpl(eClass: KCl internal actual fun KClass<*>.platformSpecificSerializerNotRegistered(): Nothing = serializerNotRegistered() -@Suppress("UNCHECKED_CAST") +internal fun Class<*>.serializerNotRegistered(): Nothing { + throw SerializationException( + "Serializer for class '${simpleName}' is not found.\n" + + "Mark the class as @Serializable or provide the serializer explicitly." + ) +} + internal actual fun KClass.constructSerializerForGivenTypeArgs(vararg args: KSerializer): KSerializer? { - val jClass = this.java - if (jClass.isEnum && jClass.isNotAnnotated()) { - return jClass.createEnumSerializer() + return java.constructSerializerForGivenTypeArgs(*args) +} + +@Suppress("UNCHECKED_CAST") +internal fun Class.constructSerializerForGivenTypeArgs(vararg args: KSerializer): KSerializer? { + if (isEnum && isNotAnnotated()) { + return createEnumSerializer() } - if (jClass.isInterface) { + if (isInterface) { return interfaceSerializer() } // Search for serializer defined on companion object. - val serializer = invokeSerializerOnCompanion(jClass, *args) + val serializer = invokeSerializerOnCompanion(this, *args) if (serializer != null) return serializer // Check whether it's serializable object - findObjectSerializer(jClass)?.let { return it } + findObjectSerializer()?.let { return it } // Search for default serializer if no serializer is defined in companion object. // It is required for named companions val fromNamedCompanion = try { - jClass.declaredClasses.singleOrNull { it.simpleName == ("\$serializer") } + declaredClasses.singleOrNull { it.simpleName == ("\$serializer") } ?.getField("INSTANCE")?.get(null) as? KSerializer } catch (e: NoSuchFieldException) { null @@ -63,31 +73,30 @@ private fun Class.isNotAnnotated(): Boolean { getAnnotation(Polymorphic::class.java) == null } -private fun KClass.polymorphicSerializer(): KSerializer? { +private fun Class.polymorphicSerializer(): KSerializer? { /* * Last resort: check for @Polymorphic or Serializable(with = PolymorphicSerializer::class) * annotations. */ - val jClass = java - if (jClass.getAnnotation(Polymorphic::class.java) != null) { - return PolymorphicSerializer(this) + if (getAnnotation(Polymorphic::class.java) != null) { + return PolymorphicSerializer(this.kotlin) } - val serializable = jClass.getAnnotation(Serializable::class.java) + val serializable = getAnnotation(Serializable::class.java) if (serializable != null && serializable.with == PolymorphicSerializer::class) { - return PolymorphicSerializer(this) + return PolymorphicSerializer(this.kotlin) } return null } -private fun KClass.interfaceSerializer(): KSerializer? { +private fun Class.interfaceSerializer(): KSerializer? { /* * Interfaces are @Polymorphic by default. * Check if it has no annotations or `@Serializable(with = PolymorphicSerializer::class)`, * otherwise bailout. */ - val serializable = java.getAnnotation(Serializable::class.java) + val serializable = getAnnotation(Serializable::class.java) if (serializable == null || serializable.with == PolymorphicSerializer::class) { - return PolymorphicSerializer(this) + return PolymorphicSerializer(this.kotlin) } return null } @@ -122,15 +131,15 @@ private fun Class.createEnumSerializer(): KSerializer? { return EnumSerializer(canonicalName, constants as Array>) as? KSerializer } -private fun findObjectSerializer(jClass: Class): KSerializer? { +private fun Class.findObjectSerializer(): KSerializer? { // Check it is an object without using kotlin-reflect val field = - jClass.declaredFields.singleOrNull { it.name == "INSTANCE" && it.type == jClass && Modifier.isStatic(it.modifiers) } + declaredFields.singleOrNull { it.name == "INSTANCE" && it.type == this && Modifier.isStatic(it.modifiers) } ?: return null // Retrieve its instance and call serializer() val instance = field.get(null) val method = - jClass.methods.singleOrNull { it.name == "serializer" && it.parameterTypes.isEmpty() && it.returnType == KSerializer::class.java } + methods.singleOrNull { it.name == "serializer" && it.parameterTypes.isEmpty() && it.returnType == KSerializer::class.java } ?: return null val result = method.invoke(instance) @Suppress("UNCHECKED_CAST") diff --git a/formats/json/jvmTest/src/kotlinx/serialization/features/SerializerByTypeTest.kt b/formats/json/jvmTest/src/kotlinx/serialization/features/SerializerByTypeTest.kt index 509c9e0c5..5227ca43a 100644 --- a/formats/json/jvmTest/src/kotlinx/serialization/features/SerializerByTypeTest.kt +++ b/formats/json/jvmTest/src/kotlinx/serialization/features/SerializerByTypeTest.kt @@ -18,7 +18,7 @@ import kotlin.test.* class SerializerByTypeTest { - val json = Json { } + private val json = Json @Serializable data class Box(val a: T) @@ -56,6 +56,12 @@ class SerializerByTypeTest { assertSerializedWithType(IntBoxToken, """{"a":42}""", b) } + @Test + fun testNestedGenericParameter() { + val b = Box(Box(239)) + assertSerializedWithType(typeTokenOf>>(), """{"a":{"a":239}}""", b) + } + @Test fun testArray() { val myArr = arrayOf("a", "b", "c") @@ -156,7 +162,6 @@ class SerializerByTypeTest { val myTriple = Triple("1", 2, Box(42)) val token = typeTokenOf>>() assertSerializedWithType(token, """{"first":"1","second":2,"third":{"a":42}}""", myTriple) - } @Test @@ -273,5 +278,9 @@ class SerializerByTypeTest { assertFailsWithMessage("for class 'NonSerializable'") { serializer(typeTokenOf>()) } + + assertFailsWithMessage("for class 'NonSerializable'") { + serializer(typeTokenOf>()) + } } }