Skip to content

Commit 65fdbc7

Browse files
authored
Serializer lookup rework (#958)
* Make @serializable and @polymorphic retain in runtime * Do not construct default serializer for explicitly annotated enum * Properly lookup serializer for interfaces * Lookup builtin serializer before contextual to provide consistent experience Fixes #928
1 parent 6224c23 commit 65fdbc7

File tree

12 files changed

+274
-59
lines changed

12 files changed

+274
-59
lines changed

Diff for: runtime/api/kotlinx-serialization-core.api

+1
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ public final class kotlinx/serialization/PolymorphicSerializer : kotlinx/seriali
9797
public fun <init> (Lkotlin/reflect/KClass;)V
9898
public fun getBaseClass ()Lkotlin/reflect/KClass;
9999
public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
100+
public fun toString ()Ljava/lang/String;
100101
}
101102

102103
public final class kotlinx/serialization/PolymorphicSerializerKt {

Diff for: runtime/commonMain/src/kotlinx/serialization/Annotations.kt

+2-2
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ import kotlin.reflect.*
6868
* @see Serializer
6969
*/
7070
@Target(AnnotationTarget.PROPERTY, AnnotationTarget.CLASS, AnnotationTarget.TYPE)
71-
@Retention(AnnotationRetention.BINARY)
71+
@Retention(AnnotationRetention.RUNTIME)
7272
public annotation class Serializable(
7373
val with: KClass<out KSerializer<*>> = KSerializer::class // Default value indicates that auto-generated serializer is used
7474
)
@@ -202,7 +202,7 @@ public annotation class UseSerializers(vararg val serializerClasses: KClass<out
202202
* with special compiler plugin support which would be added later.
203203
*/
204204
@Target(AnnotationTarget.PROPERTY, AnnotationTarget.TYPE, AnnotationTarget.CLASS)
205-
@Retention(AnnotationRetention.BINARY)
205+
@Retention(AnnotationRetention.RUNTIME)
206206
public annotation class Polymorphic
207207

208208
/**

Diff for: runtime/commonMain/src/kotlinx/serialization/PolymorphicSerializer.kt

+4
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,10 @@ public class PolymorphicSerializer<T : Any>(override val baseClass: KClass<T>) :
7474
buildSerialDescriptor("kotlinx.serialization.Polymorphic<${baseClass.simpleName}>", SerialKind.CONTEXTUAL)
7575
)
7676
}.withContext(baseClass)
77+
78+
override fun toString(): String {
79+
return "kotlinx.serialization.PolymorphicSerializer(baseClass: $baseClass)"
80+
}
7781
}
7882

7983
public fun <T : Any> AbstractPolymorphicSerializer<T>.findPolymorphicSerializer(

Diff for: runtime/commonMain/src/kotlinx/serialization/Serializers.kt

+15-12
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
package kotlinx.serialization
99

1010
import kotlinx.serialization.builtins.*
11-
import kotlinx.serialization.builtins.nullable
1211
import kotlinx.serialization.internal.*
1312
import kotlinx.serialization.modules.*
1413
import kotlin.jvm.*
@@ -35,36 +34,40 @@ public inline fun <reified T> SerializersModule.serializer(): KSerializer<T> {
3534
* [type] argument can be obtained with experimental [typeOf] method.
3635
*/
3736
public fun serializer(type: KType): KSerializer<Any?> {
38-
val result = EmptySerializersModule.serializerByKTypeImpl(type)
37+
val result = EmptySerializersModule.serializerByKTypeImpl(type) ?: type.kclass().serializerNotRegistered()
3938
return result.nullable(type.isMarkedNullable)
4039
}
4140

4241
/**
43-
* Retrieves serializer for the given [type] from the current [SerializersModule] and,
44-
* if not found, fallbacks to plain [serializer] method.
42+
* Attempts to create a serializer for the given [type] and fallbacks to [contextual][SerializersModule.getContextual]
43+
* lookup for non-serializable types.
4544
* [type] argument can be obtained with experimental [typeOf] method.
4645
*/
4746
public fun SerializersModule.serializer(type: KType): KSerializer<Any?> {
4847
val kclass = type.kclass()
4948
val isNullable = type.isMarkedNullable
50-
getContextual(kclass)?.nullable(isNullable)?.let { return it.cast() }
51-
return serializerByKTypeImpl(type).nullable(isNullable).cast()
49+
val builtin = serializerByKTypeImpl(type)
50+
if (builtin != null) {
51+
return builtin.nullable(isNullable).cast()
52+
}
53+
54+
return getContextual(kclass)?.nullable(isNullable)?.cast() ?: type.kclass().serializerNotRegistered()
5255
}
5356

54-
private fun SerializersModule.serializerByKTypeImpl(type: KType): KSerializer<Any> {
57+
private fun SerializersModule.serializerByKTypeImpl(type: KType): KSerializer<Any>? {
5558
val rootClass = type.kclass()
5659
val typeArguments = type.arguments
5760
.map { requireNotNull(it.type) { "Star projections in type arguments are not allowed, but had $type" } }
5861
return when {
59-
typeArguments.isEmpty() -> getContextual(rootClass) ?: rootClass.serializer()
60-
else -> builtinSerializer(typeArguments, rootClass)
61-
}.cast()
62+
typeArguments.isEmpty() -> rootClass.serializerOrNull() ?: getContextual(rootClass)
63+
else -> builtinSerializerOrNull(typeArguments, rootClass)
64+
}?.cast()
6265
}
6366

64-
private fun SerializersModule.builtinSerializer(
67+
private fun SerializersModule.builtinSerializerOrNull(
6568
typeArguments: List<KType>,
6669
rootClass: KClass<Any>
67-
): KSerializer<out Any> {
70+
): KSerializer<out Any>? {
6871
val serializers = typeArguments
6972
.map(::serializer)
7073
// Array is not supported, see KT-32839

Diff for: runtime/commonMain/src/kotlinx/serialization/descriptors/ContextAware.kt

+4
Original file line numberDiff line numberDiff line change
@@ -98,4 +98,8 @@ private class ContextDescriptor(
9898
result = 31 * result + serialName.hashCode()
9999
return result
100100
}
101+
102+
override fun toString(): String {
103+
return "ContextDescriptor(kClass: $kClass, original: $original)"
104+
}
101105
}

Diff for: runtime/commonMain/src/kotlinx/serialization/internal/PluginHelperInterfaces.kt

-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ public interface GeneratedSerializer<T> : KSerializer<T> {
3131
* Should not be used in any user code. Please use generated `.serializer(kSerializer1, kSerializer2, ...)`
3232
* method on a companion or top-level `serializer(KType)` function.
3333
*/
34-
@InternalSerializationApi
3534
@Deprecated("Inserted into generated code and should not be used directly", level = DeprecationLevel.HIDDEN)
3635
public interface SerializerFactory {
3736
public fun serializer(vararg typeParamsSerializers: KSerializer<*>): KSerializer<*>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/*
2+
* Copyright 2017-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
package kotlinx.serialization
6+
7+
import kotlinx.serialization.descriptors.*
8+
import kotlinx.serialization.encoding.*
9+
import kotlinx.serialization.json.*
10+
import kotlinx.serialization.test.*
11+
import kotlin.test.*
12+
13+
// This is unimplemented functionality that should be
14+
@Suppress("RemoveExplicitTypeArguments") // This is exactly what's being tested
15+
class SerializersLookupEnumTest : JsonTestBase() {
16+
@Serializable(with = EnumExternalObjectSerializer::class)
17+
enum class EnumExternalObject
18+
19+
@Serializer(forClass = EnumExternalObject::class)
20+
object EnumExternalObjectSerializer {
21+
override val descriptor: SerialDescriptor = buildSerialDescriptor("tmp", SerialKind.ENUM)
22+
23+
override fun serialize(encoder: Encoder, value: EnumExternalObject) {
24+
TODO()
25+
}
26+
27+
override fun deserialize(decoder: Decoder): EnumExternalObject {
28+
TODO()
29+
}
30+
}
31+
32+
@Serializable(with = EnumExternalClassSerializer::class)
33+
enum class EnumExternalClass
34+
35+
@Serializer(forClass = EnumExternalObject::class)
36+
class EnumExternalClassSerializer {
37+
override val descriptor: SerialDescriptor = buildSerialDescriptor("tmp", SerialKind.ENUM)
38+
39+
override fun serialize(encoder: Encoder, value: EnumExternalObject) {
40+
TODO()
41+
}
42+
43+
override fun deserialize(decoder: Decoder): EnumExternalObject {
44+
TODO()
45+
}
46+
}
47+
48+
@Polymorphic
49+
enum class EnumPolymorphic
50+
51+
@Serializable
52+
enum class PlainEnum
53+
54+
@Test
55+
fun testPlainEnum() {
56+
assertEquals(PlainEnum.serializer(), serializer<PlainEnum>())
57+
}
58+
59+
@Test
60+
fun testEnumExternalObject() {
61+
assertFailsWith<SerializationException> { (serializer<EnumExternalObject>()) }
62+
}
63+
64+
@Test
65+
fun testEnumExternalClass() {
66+
assertFailsWith<SerializationException> { serializer<EnumExternalClass>() }
67+
}
68+
69+
@Test
70+
fun testEnumPolymorphic() {
71+
jvmOnly {
72+
assertEquals(
73+
PolymorphicSerializer(EnumPolymorphic::class).descriptor,
74+
serializer<EnumPolymorphic>().descriptor
75+
)
76+
}
77+
}
78+
}

Diff for: runtime/commonTest/src/kotlinx/serialization/TypeOfSerializerLookupTest.kt renamed to runtime/commonTest/src/kotlinx/serialization/SerializersLookupTest.kt

+31-12
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import kotlin.reflect.*
1616
import kotlin.test.*
1717

1818
@Suppress("RemoveExplicitTypeArguments") // This is exactly what's being tested
19-
class TypeOfSerializerLookupTest : JsonTestBase() {
19+
class SerializersLookupTest : JsonTestBase() {
2020

2121
@Test
2222
fun testPrimitive() {
@@ -135,45 +135,64 @@ class TypeOfSerializerLookupTest : JsonTestBase() {
135135
assertSerializedWithType("{}", SampleObject)
136136
}
137137

138+
class IntBox(val i: Int)
138139

139-
class CustomIntSerializer(isNullable: Boolean) : KSerializer<Int?> {
140+
class CustomIntSerializer(isNullable: Boolean) : KSerializer<IntBox?> {
140141
override val descriptor: SerialDescriptor
141142

142143
init {
143144
val d = PrimitiveSerialDescriptor("CIS", PrimitiveKind.INT)
144145
descriptor = if (isNullable) d.nullable else d
145146
}
146147

147-
override fun serialize(encoder: Encoder, value: Int?) {
148+
override fun serialize(encoder: Encoder, value: IntBox?) {
148149
if (value == null) encoder.encodeInt(41)
149150
else encoder.encodeInt(42)
150151
}
151152

152-
override fun deserialize(decoder: Decoder): Int? {
153+
override fun deserialize(decoder: Decoder): IntBox? {
153154
TODO()
154155
}
155156
}
156157

157158
@Test
158159
fun testContextualLookup() {
159-
val module = SerializersModule { contextual(CustomIntSerializer(false).cast<Int>()) }
160+
val module = SerializersModule { contextual(CustomIntSerializer(false).cast<IntBox>()) }
160161
val json = Json { serializersModule = module }
161-
val data = listOf(listOf(1))
162+
val data = listOf(listOf(IntBox(1)))
162163
assertEquals("[[42]]", json.encodeToString(data))
163164
}
164165

165166
@Test
166167
fun testContextualLookupNullable() {
167-
val module = SerializersModule { contextual(CustomIntSerializer(true).cast<Int>()) }
168-
val serializer = module.serializer<List<List<Int?>>>()
169-
assertEquals("[[41]]", Json.encodeToString(serializer, listOf(listOf<Int?>(null))))
168+
val module = SerializersModule { contextual(CustomIntSerializer(true).cast<IntBox>()) }
169+
val serializer = module.serializer<List<List<IntBox?>>>()
170+
assertEquals("[[41]]", Json.encodeToString(serializer, listOf(listOf<IntBox?>(null))))
170171
}
171172

172173
@Test
173174
fun testContextualLookupNonNullable() {
174-
val module = SerializersModule { contextual(CustomIntSerializer(false).cast<Int>()) }
175-
val serializer = module.serializer<List<List<Int?>>>()
176-
assertEquals("[[null]]", Json.encodeToString(serializer, listOf(listOf<Int?>(null))))
175+
val module = SerializersModule { contextual(CustomIntSerializer(false).cast<IntBox>()) }
176+
val serializer = module.serializer<List<List<IntBox?>>>()
177+
assertEquals("[[null]]", Json.encodeToString(serializer, listOf(listOf<IntBox?>(null))))
178+
}
179+
180+
@Test
181+
fun testCompiledWinsOverContextual() {
182+
val contextual = object : KSerializer<Int> {
183+
override val descriptor: SerialDescriptor = Int.serializer().descriptor
184+
185+
override fun serialize(encoder: Encoder, value: Int) {
186+
fail()
187+
}
188+
189+
override fun deserialize(decoder: Decoder): Int {
190+
fail()
191+
}
192+
}
193+
val json = Json { serializersModule = SerializersModule { contextual(contextual) } }
194+
assertEquals("[[1]]", json.encodeToString(listOf(listOf<Int>(1))))
195+
assertEquals("42", json.encodeToString(42))
177196
}
178197

179198
// Tests with [constructSerializerForGivenTypeArgs] are unsupported on legacy Kotlin/JS

Diff for: runtime/jvmMain/src/kotlinx/serialization/SerializersJvm.kt

+6-7
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,13 @@ public fun serializer(type: Type): KSerializer<Any> = EmptySerializersModule.ser
2525

2626
/**
2727
* Retrieves serializer for the given reflective Java [type] using
28-
* [contextual][SerializersModule.getContextual] lookup and fallback to reflective construction.
28+
* reflective construction and [contextual][SerializersModule.getContextual] lookup for non-serializable types.
2929
*
3030
* [serializer] is intended to be used as an interoperability layer for libraries like GSON and Retrofit,
3131
* that operate with reflective Java [Type] and cannot use [typeOf].
32-
* Serializers is looked up in the contextual serializers of the module and then constructed reflectively.
3332
*
3433
* For application-level serialization, it is recommended to use `serializer<T>()` instead as it is aware of
35-
* Kotlin-specific type information, such as nullability, sealed classes and object.
34+
* Kotlin-specific type information, such as nullability, sealed classes and object singletons.
3635
*/
3736
public fun SerializersModule.serializer(type: Type): KSerializer<Any> = when (type) {
3837
is GenericArrayType -> {
@@ -59,7 +58,7 @@ public fun SerializersModule.serializer(type: Type): KSerializer<Any> = when (ty
5958
// since it uses Java TypeToken, not Kotlin one
6059
val varargs = args.map { serializer(it) as KSerializer<Any?> }.toTypedArray()
6160
(rootClass.kotlin.constructSerializerForGivenTypeArgs(*varargs) as? KSerializer<Any>)
62-
?: contextualOrReflective(rootClass.kotlin as KClass<Any>)
61+
?: reflectiveOrContextual(rootClass.kotlin as KClass<Any>)
6362
}
6463
}
6564
}
@@ -69,7 +68,7 @@ public fun SerializersModule.serializer(type: Type): KSerializer<Any> = when (ty
6968

7069
private fun SerializersModule.typeSerializer(type: Class<*>): KSerializer<Any> {
7170
return if (!type.isArray) {
72-
contextualOrReflective(type.kotlin as KClass<Any>)
71+
reflectiveOrContextual(type.kotlin as KClass<Any>)
7372
} else {
7473
val eType: Class<*> = type.componentType
7574
val s = serializer(eType)
@@ -94,8 +93,8 @@ private fun SerializersModule.genericArraySerializer(type: GenericArrayType): KS
9493
return ArraySerializer(kclass, serializer) as KSerializer<Any>
9594
}
9695

97-
private fun <T: Any> SerializersModule.contextualOrReflective(kClass: KClass<T>): KSerializer<T> {
98-
return getContextual(kClass) ?: kClass.serializer()
96+
private fun <T: Any> SerializersModule.reflectiveOrContextual(kClass: KClass<T>): KSerializer<T> {
97+
return kClass.serializerOrNull() ?: getContextual(kClass) ?: kClass.serializerNotRegistered()
9998
}
10099

101100
@Deprecated("Deprecated during serialization 1.0 API stabilization", ReplaceWith("serializer(type)"), level = DeprecationLevel.ERROR)

0 commit comments

Comments
 (0)