Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add serializerOrNull function for KType and Type arguments #1164

Merged
merged 2 commits into from
Dec 25, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions core/api/kotlinx-serialization-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,11 @@ public final class kotlinx/serialization/SerializersKt {
public static final fun serializer (Lkotlin/reflect/KType;)Lkotlinx/serialization/KSerializer;
public static final fun serializer (Lkotlinx/serialization/modules/SerializersModule;Ljava/lang/reflect/Type;)Lkotlinx/serialization/KSerializer;
public static final fun serializer (Lkotlinx/serialization/modules/SerializersModule;Lkotlin/reflect/KType;)Lkotlinx/serialization/KSerializer;
public static final fun serializerOrNull (Ljava/lang/reflect/Type;)Lkotlinx/serialization/KSerializer;
public static final fun serializerOrNull (Lkotlin/reflect/KClass;)Lkotlinx/serialization/KSerializer;
public static final fun serializerOrNull (Lkotlin/reflect/KType;)Lkotlinx/serialization/KSerializer;
public static final fun serializerOrNull (Lkotlinx/serialization/modules/SerializersModule;Ljava/lang/reflect/Type;)Lkotlinx/serialization/KSerializer;
public static final fun serializerOrNull (Lkotlinx/serialization/modules/SerializersModule;Lkotlin/reflect/KType;)Lkotlinx/serialization/KSerializer;
}

public abstract interface class kotlinx/serialization/StringFormat : kotlinx/serialization/SerialFormat {
Expand Down
69 changes: 43 additions & 26 deletions core/commonMain/src/kotlinx/serialization/Serializers.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@
package kotlinx.serialization

import kotlinx.serialization.builtins.*
import kotlinx.serialization.builtins.TripleSerializer
import kotlinx.serialization.builtins.PairSerializer
import kotlinx.serialization.builtins.MapEntrySerializer
import kotlinx.serialization.builtins.PairSerializer
import kotlinx.serialization.builtins.TripleSerializer
import kotlinx.serialization.internal.*
import kotlinx.serialization.modules.*
import kotlin.jvm.*
Expand All @@ -35,48 +35,68 @@ public inline fun <reified T> SerializersModule.serializer(): KSerializer<T> {
/**
* Creates a serializer for the given [type].
* [type] argument can be obtained with experimental [typeOf] method.
* @throws SerializationException if serializer cannot be created (provided [type] or its type argument is not serializable).
*/
@OptIn(ExperimentalSerializationApi::class)
public fun serializer(type: KType): KSerializer<Any?> {
val result = EmptySerializersModule.serializerByKTypeImpl(type) ?: type.kclass().platformSpecificSerializerNotRegistered()
return result.nullable(type.isMarkedNullable)
}
public fun serializer(type: KType): KSerializer<Any?> = EmptySerializersModule.serializer(type)

/**
* Creates a serializer for the given [type].
* [type] argument can be obtained with experimental [typeOf] method.
* Returns `null` if serializer cannot be created (provided [type] or its type argument is not serializable).
*/
@OptIn(ExperimentalSerializationApi::class)
public fun serializerOrNull(type: KType): KSerializer<Any?>? = EmptySerializersModule.serializerOrNull(type)

/**
* Attempts to create a serializer for the given [type] and fallbacks to [contextual][SerializersModule.getContextual]
* lookup for non-serializable types.
* [type] argument can be obtained with experimental [typeOf] method.
* @throws SerializationException if serializer cannot be created (provided [type] or its type argument is not serializable and is not registered in [this] module).
*/
@OptIn(ExperimentalSerializationApi::class)
public fun SerializersModule.serializer(type: KType): KSerializer<Any?> {
val kclass = type.kclass()
val isNullable = type.isMarkedNullable
val builtin = serializerByKTypeImpl(type)
if (builtin != null) {
return builtin.nullable(isNullable).cast()
}
public fun SerializersModule.serializer(type: KType): KSerializer<Any?> =
serializerByKTypeImpl(type, failOnMissingTypeArgSerializer = true) ?: type.kclass()
.platformSpecificSerializerNotRegistered()

return getContextual(kclass)?.nullable(isNullable)?.cast() ?: type.kclass().platformSpecificSerializerNotRegistered()
/**
* Attempts to create a serializer for the given [type] and fallbacks to [contextual][SerializersModule.getContextual]
* lookup for non-serializable types.
* [type] argument can be obtained with experimental [typeOf] method.
* Returns `null` if serializer cannot be created (provided [type] or its type argument is not serializable and is not registered in [this] module).
*/
@OptIn(ExperimentalSerializationApi::class)
public fun SerializersModule.serializerOrNull(type: KType): KSerializer<Any?>? {
return serializerByKTypeImpl(type, failOnMissingTypeArgSerializer = false)
}

@OptIn(ExperimentalSerializationApi::class)
private fun SerializersModule.serializerByKTypeImpl(type: KType): KSerializer<Any>? {
private fun SerializersModule.serializerByKTypeImpl(
type: KType,
failOnMissingTypeArgSerializer: Boolean
): KSerializer<Any?>? {
val rootClass = type.kclass()
val isNullable = type.isMarkedNullable
val typeArguments = type.arguments
.map { requireNotNull(it.type) { "Star projections in type arguments are not allowed, but had $type" } }
return when {
val result: KSerializer<Any>? = when {
typeArguments.isEmpty() -> rootClass.serializerOrNull() ?: getContextual(rootClass)
else -> builtinSerializerOrNull(typeArguments, rootClass)
else -> builtinSerializer(typeArguments, rootClass, failOnMissingTypeArgSerializer)
}?.cast()
return result?.nullable(isNullable)
}

@OptIn(ExperimentalSerializationApi::class)
private fun SerializersModule.builtinSerializerOrNull(
private fun SerializersModule.builtinSerializer(
typeArguments: List<KType>,
rootClass: KClass<Any>
rootClass: KClass<Any>,
failOnMissingTypeArgSerializer: Boolean
): KSerializer<out Any>? {
val serializers = typeArguments
.map(::serializer)
val serializers = if (failOnMissingTypeArgSerializer)
typeArguments.map(::serializer)
else {
typeArguments.map { serializerOrNull(it) ?: return null }
}
// Array is not supported, see KT-32839
return when (rootClass) {
Collection::class, List::class, MutableList::class, ArrayList::class -> ArrayListSerializer(serializers[0])
Expand All @@ -94,10 +114,7 @@ private fun SerializersModule.builtinSerializerOrNull(
if (isReferenceArray(rootClass)) {
return ArraySerializer<Any, Any?>(typeArguments[0].classifier as KClass<Any>, serializers[0]).cast()
}
requireNotNull(rootClass.constructSerializerForGivenTypeArgs(*serializers.toTypedArray())) {
"Can't find a method to construct serializer for type ${rootClass.simpleName}. " +
"Make sure this class is marked as @Serializable or provide serializer explicitly."
}
rootClass.constructSerializerForGivenTypeArgs(*serializers.toTypedArray())
}
}
}
Expand Down Expand Up @@ -152,7 +169,7 @@ public fun <T : Any> KClass<T>.serializer(): KSerializer<T> = serializerOrNull()
public fun <T : Any> KClass<T>.serializerOrNull(): KSerializer<T>? =
compiledSerializerImpl() ?: builtinSerializerOrNull()

private fun <T: Any> KSerializer<T>.nullable(shouldBeNullable: Boolean): KSerializer<T?> {
private fun <T : Any> KSerializer<T>.nullable(shouldBeNullable: Boolean): KSerializer<T?> {
if (shouldBeNullable) return nullable
return this as KSerializer<T?>
}
144 changes: 99 additions & 45 deletions core/jvmMain/src/kotlinx/serialization/SerializersJvm.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,31 @@ import java.lang.reflect.*
import kotlin.reflect.*

/**
* Constructs a serializer for the given reflective Java [type].
* Reflectively constructs a serializer for the given reflective Java [type].
* [serializer] is intended to be used as an interoperability layer for libraries like GSON and Retrofit,
* that operate with reflective Java [Type] and cannot use [typeOf].
*
* For application-level serialization, it is recommended to use `serializer<T>()` instead as it is aware of
* Kotlin-specific type information, such as nullability, sealed classes and object.
* Kotlin-specific type information, such as nullability, sealed classes and object singletons.
*
* @throws SerializationException if serializer cannot be created (provided [type] or its type argument is not serializable).
*/
@ExperimentalSerializationApi
public fun serializer(type: Type): KSerializer<Any> = EmptySerializersModule.serializer(type)

/**
* Reflectively constructs a serializer for the given reflective Java [type].
* [serializer] is intended to be used as an interoperability layer for libraries like GSON and Retrofit,
* that operate with reflective Java [Type] and cannot use [typeOf].
*
* For application-level serialization, it is recommended to use `serializer<T>()` instead as it is aware of
* Kotlin-specific type information, such as nullability, sealed classes and object singletons.
*
* Returns `null` if serializer cannot be created (provided [type] or its type argument is not serializable).
*/
@ExperimentalSerializationApi
public fun serializerOrNull(type: Type): KSerializer<Any>? = EmptySerializersModule.serializerOrNull(type)

/**
* Retrieves serializer for the given reflective Java [type] using
* reflective construction and [contextual][SerializersModule.getContextual] lookup for non-serializable types.
Expand All @@ -36,71 +51,101 @@ public fun serializer(type: Type): KSerializer<Any> = EmptySerializersModule.ser
*
* For application-level serialization, it is recommended to use `serializer<T>()` instead as it is aware of
* Kotlin-specific type information, such as nullability, sealed classes and object singletons.
*
* @throws SerializationException if serializer cannot be created (provided [type] or its type argument is not serializable).
*/
@ExperimentalSerializationApi
public fun SerializersModule.serializer(type: Type): KSerializer<Any> = when (type) {
is GenericArrayType -> {
genericArraySerializer(type)
}
is Class<*> -> typeSerializer(type)
is ParameterizedType -> {
val rootClass = (type.rawType as Class<*>)
val args = (type.actualTypeArguments)
when {
Set::class.java.isAssignableFrom(rootClass) -> SetSerializer(serializer(args[0])) as KSerializer<Any>
List::class.java.isAssignableFrom(rootClass) || Collection::class.java.isAssignableFrom(rootClass) -> ListSerializer(serializer(args[0])) as KSerializer<Any>
Map::class.java.isAssignableFrom(rootClass) -> MapSerializer(
serializer(args[0]),
serializer(args[1])
) as KSerializer<Any>
Map.Entry::class.java.isAssignableFrom(rootClass) -> MapEntrySerializer(
serializer(args[0]),
serializer(args[1])
) as KSerializer<Any>
Pair::class.java.isAssignableFrom(rootClass) -> PairSerializer(
serializer(args[0]),
serializer(args[1])
) as KSerializer<Any>
Triple::class.java.isAssignableFrom(rootClass) -> TripleSerializer(
serializer(args[0]),
serializer(args[1]),
serializer(args[2])
) as KSerializer<Any>
public fun SerializersModule.serializer(type: Type): KSerializer<Any> =
serializerByJavaTypeImpl(type, failOnMissingTypeArgSerializer = true) ?: type.kclass().serializerNotRegistered()

else -> {
// 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 = args.map { serializer(it) as KSerializer<Any?> }.toTypedArray()
(rootClass.kotlin.constructSerializerForGivenTypeArgs(*varargs) as? KSerializer<Any>)
?: reflectiveOrContextual(rootClass.kotlin as KClass<Any>)
/**
* Retrieves serializer for the given reflective Java [type] using
* reflective construction and [contextual][SerializersModule.getContextual] lookup for non-serializable types.
*
* [serializer] is intended to be used as an interoperability layer for libraries like GSON and Retrofit,
* that operate with reflective Java [Type] and cannot use [typeOf].
*
* For application-level serialization, it is recommended to use `serializer<T>()` instead as it is aware of
* Kotlin-specific type information, such as nullability, sealed classes and object singletons.
*
* Returns `null` if serializer cannot be created (provided [type] or its type argument is not serializable).
*/
@ExperimentalSerializationApi
public fun SerializersModule.serializerOrNull(type: Type): KSerializer<Any>? =
serializerByJavaTypeImpl(type, failOnMissingTypeArgSerializer = false)

@OptIn(ExperimentalSerializationApi::class)
private fun SerializersModule.serializerByJavaTypeImpl(type: Type, failOnMissingTypeArgSerializer: Boolean = true): KSerializer<Any>? =
when (type) {
is GenericArrayType -> {
genericArraySerializer(type, failOnMissingTypeArgSerializer)
}
is Class<*> -> typeSerializer(type, failOnMissingTypeArgSerializer)
is ParameterizedType -> {
val rootClass = (type.rawType as Class<*>)
val args = (type.actualTypeArguments)
val argsSerializers =
if (failOnMissingTypeArgSerializer) args.map { serializer(it) } else args.map { serializerOrNull(it) ?: return null }
when {
Set::class.java.isAssignableFrom(rootClass) -> SetSerializer(argsSerializers[0]) as KSerializer<Any>
List::class.java.isAssignableFrom(rootClass) || Collection::class.java.isAssignableFrom(rootClass) -> ListSerializer(
argsSerializers[0]
) as KSerializer<Any>
Map::class.java.isAssignableFrom(rootClass) -> MapSerializer(
argsSerializers[0],
argsSerializers[1]
) as KSerializer<Any>
Map.Entry::class.java.isAssignableFrom(rootClass) -> MapEntrySerializer(
argsSerializers[0],
argsSerializers[1]
) as KSerializer<Any>
Pair::class.java.isAssignableFrom(rootClass) -> PairSerializer(
argsSerializers[0],
argsSerializers[1]
) as KSerializer<Any>
Triple::class.java.isAssignableFrom(rootClass) -> TripleSerializer(
argsSerializers[0],
argsSerializers[1],
argsSerializers[2]
) as KSerializer<Any>

else -> {
// 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?> }.toTypedArray()
(rootClass.kotlin.constructSerializerForGivenTypeArgs(*varargs) as? KSerializer<Any>)
?: reflectiveOrContextual(rootClass.kotlin as KClass<Any>)
}
}
}
is WildcardType -> serializerByJavaTypeImpl(type.upperBounds.first())
else -> throw IllegalArgumentException("typeToken should be an instance of Class<?>, GenericArray, ParametrizedType or WildcardType, but actual type is $type ${type::class}")
}
is WildcardType -> serializer(type.upperBounds.first())
else -> throw IllegalArgumentException("typeToken should be an instance of Class<?>, GenericArray, ParametrizedType or WildcardType, but actual type is $type ${type::class}")
}

@OptIn(ExperimentalSerializationApi::class)
private fun SerializersModule.typeSerializer(type: Class<*>): KSerializer<Any> {
private fun SerializersModule.typeSerializer(type: Class<*>, failOnMissingTypeArgSerializer: Boolean): KSerializer<Any>? {
return if (!type.isArray) {
reflectiveOrContextual(type.kotlin as KClass<Any>)
} else {
val eType: Class<*> = type.componentType
val s = serializer(eType)
val s = if (failOnMissingTypeArgSerializer) serializer(eType) else (serializerOrNull(eType) ?: return null)
val arraySerializer = ArraySerializer(eType.kotlin as KClass<Any>, s)
arraySerializer as KSerializer<Any>
}
}

@OptIn(ExperimentalSerializationApi::class)
private fun SerializersModule.genericArraySerializer(type: GenericArrayType): KSerializer<Any> {
private fun SerializersModule.genericArraySerializer(
type: GenericArrayType,
failOnMissingTypeArgSerializer: Boolean
): KSerializer<Any>? {
val eType = type.genericComponentType.let {
when (it) {
is WildcardType -> it.upperBounds.first()
else -> it
}
}
val serializer = serializer(eType)
val serializer = if (failOnMissingTypeArgSerializer) serializer(eType) else (serializerOrNull(eType) ?: return null)
val kclass = when (eType) {
is ParameterizedType -> (eType.rawType as Class<*>).kotlin
is KClass<*> -> eType
Expand All @@ -110,6 +155,15 @@ private fun SerializersModule.genericArraySerializer(type: GenericArrayType): KS
}

@OptIn(ExperimentalSerializationApi::class)
private fun <T: Any> SerializersModule.reflectiveOrContextual(kClass: KClass<T>): KSerializer<T> {
return kClass.serializerOrNull() ?: getContextual(kClass) ?: kClass.serializerNotRegistered()
private fun <T : Any> SerializersModule.reflectiveOrContextual(kClass: KClass<T>): KSerializer<T>? {
return kClass.serializerOrNull() ?: getContextual(kClass)
}

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()
else -> throw IllegalArgumentException("typeToken should be an instance of Class<?>, GenericArray, ParametrizedType or WildcardType, but actual type is $it ${it::class}")
}
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,29 @@ class SerializersLookupTest : JsonTestBase() {
assertEquals("42", json.encodeToString(42))
}

class NonSerializable

class NonSerializableBox<T>(val boxed: T)

@Test
fun testLookupFail() {
assertNull(serializerOrNull(typeOf<NonSerializable>()))
assertNull(serializerOrNull(typeOf<NonSerializableBox<String>>()))
assertNull(serializerOrNull(typeOf<Box<NonSerializable>>()))

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

assertFailsWithMessage<SerializationException>("for class 'NonSerializableBox'") {
serializer(typeOf<NonSerializableBox<String>>())
}

assertFailsWithMessage<SerializationException>("for class 'NonSerializable'") {
serializer(typeOf<Box<NonSerializable>>())
}
}

// Tests with [constructSerializerForGivenTypeArgs] are unsupported on legacy Kotlin/JS
private inline fun noLegacyJs(test: () -> Unit) {
if (!isJsLegacy()) test()
Expand All @@ -224,6 +247,8 @@ class SerializersLookupTest : JsonTestBase() {
) {
val serial = serializer<T>()
assertEquals(expected, json.encodeToString(serial, value))
val serial2 = requireNotNull(serializerOrNull(typeOf<T>())) { "Expected serializer to be found" }
assertEquals(expected, json.encodeToString(serial2, value))
}

inline fun <T> KSerializer<*>.cast(): KSerializer<T> = this as KSerializer<T>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,9 +96,9 @@ abstract class JsonTestBase {
assertEquals(data, deserialized)
}
}
}

inline fun <reified T : Throwable> assertFailsWithMessage(message: String, block: () -> Unit) {
val exception = assertFailsWith(T::class, null, block)
assertTrue(exception.message!!.contains(message), "Expected message '${exception.message}' to contain substring '$message'")
}
inline fun <reified T : Throwable> assertFailsWithMessage(message: String, block: () -> Unit) {
val exception = assertFailsWith(T::class, null, block)
assertTrue(exception.message!!.contains(message), "Expected message '${exception.message}' to contain substring '$message'")
}
Loading