Skip to content

Commit

Permalink
Best-effort kotlin reflect avoidance in serializer(Type)
Browse files Browse the repository at this point in the history
Fixes #1819
  • Loading branch information
qwwdfsad committed Jan 18, 2022
1 parent 63fe3ff commit 2e960d0
Show file tree
Hide file tree
Showing 5 changed files with 60 additions and 36 deletions.
2 changes: 2 additions & 0 deletions core/commonMain/src/kotlinx/serialization/Annotations.kt
Expand Up @@ -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
Expand All @@ -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
7 changes: 3 additions & 4 deletions core/commonMain/src/kotlinx/serialization/Serializers.kt
Expand Up @@ -126,7 +126,6 @@ internal fun <T : Any> SerializersModule.reflectiveOrContextual(kClass: KClass<T
return kClass.serializerOrNull() ?: getContextual(kClass, typeArgumentsSerializers)
}


/**
* Retrieves a [KSerializer] for the given [KClass].
* The given class must be annotated with [Serializable] or be one of the built-in types.
Expand All @@ -137,7 +136,7 @@ internal fun <T : Any> SerializersModule.reflectiveOrContextual(kClass: KClass<T
*
* The recommended way to retrieve the serializer is inline [serializer] function and [`serializer(KType)`][serializer]
*
* 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" and have platform and reflection specific limitations.
*
* ### Constraints
Expand All @@ -161,11 +160,11 @@ public fun <T : Any> KClass<T>.serializer(): KSerializer<T> = 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
Expand Down
25 changes: 15 additions & 10 deletions core/jvmMain/src/kotlinx/serialization/SerializersJvm.kt
Expand Up @@ -56,7 +56,7 @@ public fun serializerOrNull(type: Type): KSerializer<Any>? = EmptySerializersMod
*/
@ExperimentalSerializationApi
public fun SerializersModule.serializer(type: Type): KSerializer<Any> =
serializerByJavaTypeImpl(type, failOnMissingTypeArgSerializer = true) ?: type.kclass().serializerNotRegistered()
serializerByJavaTypeImpl(type, failOnMissingTypeArgSerializer = true) ?: type.prettyClass().serializerNotRegistered()

/**
* Retrieves serializer for the given reflective Java [type] using
Expand Down Expand Up @@ -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<Any?> }
(rootClass.kotlin.constructSerializerForGivenTypeArgs(*(varargs.toTypedArray())) as? KSerializer<Any>)
?: reflectiveOrContextual(rootClass.kotlin as KClass<Any>, varargs)
reflectiveOrContextual(rootClass as Class<Any>, varargs)
}
}
}
Expand All @@ -130,10 +129,17 @@ private fun SerializersModule.typeSerializer(type: Class<*>, failOnMissingTypeAr
val arraySerializer = ArraySerializer(eType.kotlin as KClass<Any>, s)
arraySerializer as KSerializer<Any>
} else {
reflectiveOrContextual(type.kotlin as KClass<Any>, emptyList())
reflectiveOrContextual(type as Class<Any>, emptyList())
}
}

@OptIn(ExperimentalSerializationApi::class)
private fun <T : Any> SerializersModule.reflectiveOrContextual(jClass: Class<T>, typeArgumentsSerializers: List<KSerializer<Any?>>): KSerializer<T>? {
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,
Expand All @@ -154,11 +160,10 @@ private fun SerializersModule.genericArraySerializer(
return ArraySerializer(kclass, serializer) as KSerializer<Any>
}

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}")
}
49 changes: 29 additions & 20 deletions core/jvmMain/src/kotlinx/serialization/internal/Platform.kt
Expand Up @@ -28,24 +28,34 @@ internal actual fun <T : Any, E : T?> ArrayList<E>.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 <T : Any> KClass<T>.constructSerializerForGivenTypeArgs(vararg args: KSerializer<Any?>): KSerializer<T>? {
val jClass = this.java
if (jClass.isEnum && jClass.isNotAnnotated()) {
return jClass.createEnumSerializer()
return java.constructSerializerForGivenTypeArgs(*args)
}

@Suppress("UNCHECKED_CAST")
internal fun <T: Any> Class<T>.constructSerializerForGivenTypeArgs(vararg args: KSerializer<Any?>): KSerializer<T>? {
if (isEnum && isNotAnnotated()) {
return createEnumSerializer()
}
if (jClass.isInterface) {
if (isInterface) {
return interfaceSerializer()
}
// Search for serializer defined on companion object.
val serializer = invokeSerializerOnCompanion<T>(jClass, *args)
val serializer = invokeSerializerOnCompanion<T>(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<T>
} catch (e: NoSuchFieldException) {
null
Expand All @@ -63,31 +73,30 @@ private fun <T: Any> Class<T>.isNotAnnotated(): Boolean {
getAnnotation(Polymorphic::class.java) == null
}

private fun <T: Any> KClass<T>.polymorphicSerializer(): KSerializer<T>? {
private fun <T: Any> Class<T>.polymorphicSerializer(): KSerializer<T>? {
/*
* 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 <T: Any> KClass<T>.interfaceSerializer(): KSerializer<T>? {
private fun <T: Any> Class<T>.interfaceSerializer(): KSerializer<T>? {
/*
* 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
}
Expand Down Expand Up @@ -122,15 +131,15 @@ private fun <T : Any> Class<T>.createEnumSerializer(): KSerializer<T>? {
return EnumSerializer(canonicalName, constants as Array<out Enum<*>>) as? KSerializer<T>
}

private fun <T : Any> findObjectSerializer(jClass: Class<T>): KSerializer<T>? {
private fun <T : Any> Class<T>.findObjectSerializer(): KSerializer<T>? {
// 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")
Expand Down
Expand Up @@ -18,7 +18,7 @@ import kotlin.test.*

class SerializerByTypeTest {

val json = Json { }
private val json = Json

@Serializable
data class Box<out T>(val a: T)
Expand Down Expand Up @@ -56,6 +56,12 @@ class SerializerByTypeTest {
assertSerializedWithType(IntBoxToken, """{"a":42}""", b)
}

@Test
fun testNestedGenericParameter() {
val b = Box(Box(239))
assertSerializedWithType(typeTokenOf<Box<Box<Int>>>(), """{"a":{"a":239}}""", b)
}

@Test
fun testArray() {
val myArr = arrayOf("a", "b", "c")
Expand Down Expand Up @@ -156,7 +162,6 @@ class SerializerByTypeTest {
val myTriple = Triple("1", 2, Box(42))
val token = typeTokenOf<Triple<String, Int, Box<Int>>>()
assertSerializedWithType(token, """{"first":"1","second":2,"third":{"a":42}}""", myTriple)

}

@Test
Expand Down Expand Up @@ -273,5 +278,9 @@ class SerializerByTypeTest {
assertFailsWithMessage<SerializationException>("for class 'NonSerializable'") {
serializer(typeTokenOf<kotlinx.serialization.Box<NonSerializable>>())
}

assertFailsWithMessage<SerializationException>("for class 'NonSerializable'") {
serializer(typeTokenOf<Array<NonSerializable>>())
}
}
}

0 comments on commit 2e960d0

Please sign in to comment.