From 08d687feace9b4893e70bb3179dda6542ce18bc3 Mon Sep 17 00:00:00 2001 From: Leonid Startsev Date: Mon, 26 Oct 2020 20:30:37 +0300 Subject: [PATCH 1/2] Add serializerOrNull function for KType and Type arguments to avoid building framework logic on catching exceptions. Fixes #1163 --- core/api/kotlinx-serialization-core.api | 4 + .../src/kotlinx/serialization/Serializers.kt | 69 +++++---- .../kotlinx/serialization/SerializersJvm.kt | 144 ++++++++++++------ .../serialization/SerializersLookupTest.kt | 23 +++ .../serialization/json/JsonTestBase.kt | 8 +- .../features/SerializerByTypeTest.kt | 23 +++ 6 files changed, 196 insertions(+), 75 deletions(-) diff --git a/core/api/kotlinx-serialization-core.api b/core/api/kotlinx-serialization-core.api index 903af3626d..4dd3a1ba09 100644 --- a/core/api/kotlinx-serialization-core.api +++ b/core/api/kotlinx-serialization-core.api @@ -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 { diff --git a/core/commonMain/src/kotlinx/serialization/Serializers.kt b/core/commonMain/src/kotlinx/serialization/Serializers.kt index cae088deac..cc1b347aa9 100644 --- a/core/commonMain/src/kotlinx/serialization/Serializers.kt +++ b/core/commonMain/src/kotlinx/serialization/Serializers.kt @@ -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.* @@ -35,48 +35,68 @@ public inline fun SerializersModule.serializer(): KSerializer { /** * 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 { - val result = EmptySerializersModule.serializerByKTypeImpl(type) ?: type.kclass().platformSpecificSerializerNotRegistered() - return result.nullable(type.isMarkedNullable) -} +public fun serializer(type: KType): KSerializer = 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? = 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 { - 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 = + 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? { + return serializerByKTypeImpl(type, failOnMissingTypeArgSerializer = false) } @OptIn(ExperimentalSerializationApi::class) -private fun SerializersModule.serializerByKTypeImpl(type: KType): KSerializer? { +private fun SerializersModule.serializerByKTypeImpl( + type: KType, + failOnMissingTypeArgSerializer: Boolean +): KSerializer? { 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? = 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, - rootClass: KClass + rootClass: KClass, + failOnMissingTypeArgSerializer: Boolean ): KSerializer? { - 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]) @@ -94,10 +114,7 @@ private fun SerializersModule.builtinSerializerOrNull( if (isReferenceArray(rootClass)) { return ArraySerializer(typeArguments[0].classifier as KClass, 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()) } } } @@ -152,7 +169,7 @@ public fun KClass.serializer(): KSerializer = serializerOrNull() public fun KClass.serializerOrNull(): KSerializer? = compiledSerializerImpl() ?: builtinSerializerOrNull() -private fun KSerializer.nullable(shouldBeNullable: Boolean): KSerializer { +private fun KSerializer.nullable(shouldBeNullable: Boolean): KSerializer { if (shouldBeNullable) return nullable return this as KSerializer } diff --git a/core/jvmMain/src/kotlinx/serialization/SerializersJvm.kt b/core/jvmMain/src/kotlinx/serialization/SerializersJvm.kt index 901e633d1b..34943229c1 100644 --- a/core/jvmMain/src/kotlinx/serialization/SerializersJvm.kt +++ b/core/jvmMain/src/kotlinx/serialization/SerializersJvm.kt @@ -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()` 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 = 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()` 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? = EmptySerializersModule.serializerOrNull(type) + /** * Retrieves serializer for the given reflective Java [type] using * reflective construction and [contextual][SerializersModule.getContextual] lookup for non-serializable types. @@ -36,71 +51,101 @@ public fun serializer(type: Type): KSerializer = EmptySerializersModule.ser * * For application-level serialization, it is recommended to use `serializer()` 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 = 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 - List::class.java.isAssignableFrom(rootClass) || Collection::class.java.isAssignableFrom(rootClass) -> ListSerializer(serializer(args[0])) as KSerializer - Map::class.java.isAssignableFrom(rootClass) -> MapSerializer( - serializer(args[0]), - serializer(args[1]) - ) as KSerializer - Map.Entry::class.java.isAssignableFrom(rootClass) -> MapEntrySerializer( - serializer(args[0]), - serializer(args[1]) - ) as KSerializer - Pair::class.java.isAssignableFrom(rootClass) -> PairSerializer( - serializer(args[0]), - serializer(args[1]) - ) as KSerializer - Triple::class.java.isAssignableFrom(rootClass) -> TripleSerializer( - serializer(args[0]), - serializer(args[1]), - serializer(args[2]) - ) as KSerializer +public fun SerializersModule.serializer(type: Type): KSerializer = + 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 }.toTypedArray() - (rootClass.kotlin.constructSerializerForGivenTypeArgs(*varargs) as? KSerializer) - ?: reflectiveOrContextual(rootClass.kotlin as KClass) +/** + * 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()` 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? = + serializerByJavaTypeImpl(type, failOnMissingTypeArgSerializer = false) + +@OptIn(ExperimentalSerializationApi::class) +private fun SerializersModule.serializerByJavaTypeImpl(type: Type, failOnMissingTypeArgSerializer: Boolean = true): KSerializer? = + 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 + List::class.java.isAssignableFrom(rootClass) || Collection::class.java.isAssignableFrom(rootClass) -> ListSerializer( + argsSerializers[0] + ) as KSerializer + Map::class.java.isAssignableFrom(rootClass) -> MapSerializer( + argsSerializers[0], + argsSerializers[1] + ) as KSerializer + Map.Entry::class.java.isAssignableFrom(rootClass) -> MapEntrySerializer( + argsSerializers[0], + argsSerializers[1] + ) as KSerializer + Pair::class.java.isAssignableFrom(rootClass) -> PairSerializer( + argsSerializers[0], + argsSerializers[1] + ) as KSerializer + Triple::class.java.isAssignableFrom(rootClass) -> TripleSerializer( + argsSerializers[0], + argsSerializers[1], + argsSerializers[2] + ) as KSerializer + + 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 }.toTypedArray() + (rootClass.kotlin.constructSerializerForGivenTypeArgs(*varargs) as? KSerializer) + ?: reflectiveOrContextual(rootClass.kotlin as KClass) + } } } + 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 { +private fun SerializersModule.typeSerializer(type: Class<*>, failOnMissingTypeArgSerializer: Boolean): KSerializer? { return if (!type.isArray) { reflectiveOrContextual(type.kotlin as KClass) } 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, s) arraySerializer as KSerializer } } @OptIn(ExperimentalSerializationApi::class) -private fun SerializersModule.genericArraySerializer(type: GenericArrayType): KSerializer { +private fun SerializersModule.genericArraySerializer( + type: GenericArrayType, + failOnMissingTypeArgSerializer: Boolean +): KSerializer? { 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 @@ -110,6 +155,15 @@ private fun SerializersModule.genericArraySerializer(type: GenericArrayType): KS } @OptIn(ExperimentalSerializationApi::class) -private fun SerializersModule.reflectiveOrContextual(kClass: KClass): KSerializer { - return kClass.serializerOrNull() ?: getContextual(kClass) ?: kClass.serializerNotRegistered() +private fun SerializersModule.reflectiveOrContextual(kClass: KClass): KSerializer? { + 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}") } diff --git a/formats/json/commonTest/src/kotlinx/serialization/SerializersLookupTest.kt b/formats/json/commonTest/src/kotlinx/serialization/SerializersLookupTest.kt index c40d8f9f9d..3776143d66 100644 --- a/formats/json/commonTest/src/kotlinx/serialization/SerializersLookupTest.kt +++ b/formats/json/commonTest/src/kotlinx/serialization/SerializersLookupTest.kt @@ -212,6 +212,29 @@ class SerializersLookupTest : JsonTestBase() { assertEquals("42", json.encodeToString(42)) } + class NonSerializable + + class NonSerializableBox(val boxed: T) + + @Test + fun testLookupFail() { + assertNull(serializerOrNull(typeOf())) + assertNull(serializerOrNull(typeOf>())) + assertNull(serializerOrNull(typeOf>())) + + assertFailsWithMessage("for class 'NonSerializable'") { + serializer(typeOf()) + } + + assertFailsWithMessage("for class 'NonSerializableBox'") { + serializer(typeOf>()) + } + + assertFailsWithMessage("for class 'NonSerializable'") { + serializer(typeOf>()) + } + } + // Tests with [constructSerializerForGivenTypeArgs] are unsupported on legacy Kotlin/JS private inline fun noLegacyJs(test: () -> Unit) { if (!isJsLegacy()) test() diff --git a/formats/json/commonTest/src/kotlinx/serialization/json/JsonTestBase.kt b/formats/json/commonTest/src/kotlinx/serialization/json/JsonTestBase.kt index 7e69a708a3..64fa9d3cb9 100644 --- a/formats/json/commonTest/src/kotlinx/serialization/json/JsonTestBase.kt +++ b/formats/json/commonTest/src/kotlinx/serialization/json/JsonTestBase.kt @@ -96,9 +96,9 @@ abstract class JsonTestBase { assertEquals(data, deserialized) } } +} - inline fun 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 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'") } diff --git a/formats/json/jvmTest/src/kotlinx/serialization/features/SerializerByTypeTest.kt b/formats/json/jvmTest/src/kotlinx/serialization/features/SerializerByTypeTest.kt index b29c6694aa..e8be2c8e51 100644 --- a/formats/json/jvmTest/src/kotlinx/serialization/features/SerializerByTypeTest.kt +++ b/formats/json/jvmTest/src/kotlinx/serialization/features/SerializerByTypeTest.kt @@ -256,4 +256,27 @@ class SerializerByTypeTest { assertEquals("[[1]]", Json.encodeToString(serializer, listOf(listOf(1)))) assertEquals("42", Json.encodeToString(module.serializer(typeTokenOf()), 42)) } + + class NonSerializable + + class NonSerializableBox(val boxed: T) + + @Test + fun testLookupFail() { + assertNull(serializerOrNull(typeTokenOf())) + assertNull(serializerOrNull(typeTokenOf>())) + assertNull(serializerOrNull(typeTokenOf>())) + + assertFailsWithMessage("for class 'NonSerializable'") { + serializer(typeTokenOf()) + } + + assertFailsWithMessage("for class 'NonSerializableBox'") { + serializer(typeTokenOf>()) + } + + assertFailsWithMessage("for class 'NonSerializable'") { + serializer(typeTokenOf>()) + } + } } From deee048d3e6b4e1bffe30e033f05bf4c94c1b426 Mon Sep 17 00:00:00 2001 From: Leonid Startsev Date: Wed, 23 Dec 2020 18:01:29 +0300 Subject: [PATCH 2/2] Use serializerOrNull function in other tests, too --- .../serialization/SerializersLookupTest.kt | 2 + .../features/SerializerByTypeTest.kt | 71 ++++++++----------- 2 files changed, 30 insertions(+), 43 deletions(-) diff --git a/formats/json/commonTest/src/kotlinx/serialization/SerializersLookupTest.kt b/formats/json/commonTest/src/kotlinx/serialization/SerializersLookupTest.kt index 3776143d66..3fb8e3a6e7 100644 --- a/formats/json/commonTest/src/kotlinx/serialization/SerializersLookupTest.kt +++ b/formats/json/commonTest/src/kotlinx/serialization/SerializersLookupTest.kt @@ -247,6 +247,8 @@ class SerializersLookupTest : JsonTestBase() { ) { val serial = serializer() assertEquals(expected, json.encodeToString(serial, value)) + val serial2 = requireNotNull(serializerOrNull(typeOf())) { "Expected serializer to be found" } + assertEquals(expected, json.encodeToString(serial2, value)) } inline fun KSerializer<*>.cast(): KSerializer = this as KSerializer diff --git a/formats/json/jvmTest/src/kotlinx/serialization/features/SerializerByTypeTest.kt b/formats/json/jvmTest/src/kotlinx/serialization/features/SerializerByTypeTest.kt index e8be2c8e51..9405b27a11 100644 --- a/formats/json/jvmTest/src/kotlinx/serialization/features/SerializerByTypeTest.kt +++ b/formats/json/jvmTest/src/kotlinx/serialization/features/SerializerByTypeTest.kt @@ -12,6 +12,7 @@ import kotlinx.serialization.json.* import kotlinx.serialization.modules.* import org.junit.Test import java.lang.reflect.* +import kotlin.reflect.* import kotlin.test.* class SerializerByTypeTest { @@ -51,18 +52,14 @@ class SerializerByTypeTest { @Test fun testGenericParameter() { val b = Box(42) - val serial = serializer(IntBoxToken) - val s = json.encodeToString(serial, b) - assertEquals("""{"a":42}""", s) + assertSerializedWithType(IntBoxToken, """{"a":42}""", b) } @Test fun testArray() { val myArr = arrayOf("a", "b", "c") val token = myArr::class.java - val serial = serializer(token) - val s = json.encodeToString(serial, myArr) - assertEquals("""["a","b","c"]""", s) + assertSerializedWithType(token, """["a","b","c"]""", myArr) } @Test @@ -73,9 +70,7 @@ class SerializerByTypeTest { override fun getOwnerType(): Type? = null override fun getActualTypeArguments(): Array = arrayOf(String::class.java) } - val serial = serializer(token) - val s = json.encodeToString(serial, myArr) - assertEquals("""["a","b","c"]""", s) + assertSerializedWithType(token, """["a","b","c"]""", myArr) } @Test @@ -86,9 +81,7 @@ class SerializerByTypeTest { override fun getOwnerType(): Type? = null override fun getActualTypeArguments(): Array = arrayOf(String::class.java) } - val serial = serializer(token) - val s = json.encodeToString(serial, myArr) - assertEquals("""["a","b","c"]""", s) + assertSerializedWithType(token, """["a","b","c"]""", myArr) } @@ -96,88 +89,70 @@ class SerializerByTypeTest { fun testReifiedArrayResolving() { val myArr = arrayOf("a", "b", "c") val token = typeTokenOf>() - val serial = serializer(token) - val s = json.encodeToString(serial, myArr) - assertEquals("""["a","b","c"]""", s) + assertSerializedWithType(token, """["a","b","c"]""", myArr) } @Test fun testReifiedListResolving() { val myList = listOf("a", "b", "c") val token = typeTokenOf>() - val serial = serializer(token) - val s = json.encodeToString(serial, myList) - assertEquals("""["a","b","c"]""", s) + assertSerializedWithType(token, """["a","b","c"]""", myList) } @Test fun testReifiedSetResolving() { val mySet = setOf("a", "b", "c", "c") val token = typeTokenOf>() - val serial = serializer(token) - val s = json.encodeToString(serial, mySet) - assertEquals("""["a","b","c"]""", s) + assertSerializedWithType(token, """["a","b","c"]""", mySet) } @Test fun testReifiedMapResolving() { val myMap = mapOf("a" to Data(listOf("c"), Box(6))) val token = typeTokenOf>() - val serial = serializer(token) - val s = json.encodeToString(serial, myMap) - assertEquals("""{"a":{"l":["c"],"b":{"a":6}}}""", s) + assertSerializedWithType(token, """{"a":{"l":["c"],"b":{"a":6}}}""",myMap) } @Test fun testNestedListResolving() { val myList = listOf(listOf(listOf(1, 2, 3)), listOf()) val token = typeTokenOf>>>() - val serial = serializer(token) - val s = json.encodeToString(serial, myList) - assertEquals("[[[1,2,3]],[]]", s) + assertSerializedWithType(token, "[[[1,2,3]],[]]", myList) } @Test fun testNestedArrayResolving() { val myList = arrayOf(arrayOf(arrayOf(1, 2, 3)), arrayOf()) val token = typeTokenOf>>>() - val serial = serializer(token) - val s = json.encodeToString(serial, myList) - assertEquals("[[[1,2,3]],[]]", s) + assertSerializedWithType(token, "[[[1,2,3]],[]]", myList) } @Test fun testNestedMixedResolving() { val myList = arrayOf(listOf(arrayOf(1, 2, 3)), listOf()) val token = typeTokenOf>>>() - val serial = serializer(token) - val s = json.encodeToString(serial, myList) - assertEquals("[[[1,2,3]],[]]", s) + assertSerializedWithType(token, "[[[1,2,3]],[]]", myList) } @Test fun testPair() { val myPair = "42" to 42 val token = typeTokenOf>() - val serial = serializer(token) - assertEquals("""{"first":"42","second":42}""", json.encodeToString(serial, myPair)) + assertSerializedWithType(token, """{"first":"42","second":42}""", myPair) } @Test fun testTriple() { val myTriple = Triple("1", 2, Box(42)) val token = typeTokenOf>>() - val serial = serializer(token) - assertEquals("""{"first":"1","second":2,"third":{"a":42}}""", json.encodeToString(serial, myTriple)) + assertSerializedWithType(token, """{"first":"1","second":2,"third":{"a":42}}""", myTriple) } @Test fun testGenericInHolder() { val b = Data(listOf("a", "b", "c"), Box(42)) - val serial = serializer(Data::class.java) - val s = json.encodeToString(serial, b) - assertEquals("""{"l":["a","b","c"],"b":{"a":42}}""", s) + assertSerializedWithType(Data::class.java,"""{"l":["a","b","c"],"b":{"a":42}}""", b ) } @Test @@ -189,9 +164,7 @@ class SerializerByTypeTest { @Test fun testNamedCompanion() { val namedCompanion = WithNamedCompanion(1) - val serial = serializer(WithNamedCompanion::class.java) - val s = json.encodeToString(serial, namedCompanion) - assertEquals("""{"a":1}""", s) + assertSerializedWithType(WithNamedCompanion::class.java, """{"a":1}""", namedCompanion) } @Test @@ -208,6 +181,18 @@ class SerializerByTypeTest { assertEquals(SerializableObject.serializer().descriptor, serial.descriptor) } + @Suppress("UNCHECKED_CAST") + private fun assertSerializedWithType( + token: Type, + expected: String, + value: T, + ) { + val serial = serializer(token) as KSerializer + assertEquals(expected, json.encodeToString(serial, value)) + val serial2 = requireNotNull(serializerOrNull(token)) { "Expected serializer to be found" } + assertEquals(expected, json.encodeToString(serial2 as KSerializer, value)) + } + @PublishedApi internal open class TypeBase