Skip to content

Commit d15dfee

Browse files
authored
Correctly handle serial name conflict for different classes (#2856)
in SerializersModule.overwriteWith. Previous KClass should be removed from the map as well to avoid creating a module with asymmetric mappings. Fixes #2820
1 parent d266e05 commit d15dfee

File tree

3 files changed

+51
-26
lines changed

3 files changed

+51
-26
lines changed

core/commonMain/src/kotlinx/serialization/modules/SerializersModuleBuilders.kt

Lines changed: 17 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -192,39 +192,30 @@ public class SerializersModuleBuilder @PublishedApi internal constructor() : Ser
192192
concreteSerializer: KSerializer<Sub>,
193193
allowOverwrite: Boolean = false
194194
) {
195-
// Check for overwrite
196195
val name = concreteSerializer.descriptor.serialName
197196
val baseClassSerializers = polyBase2Serializers.getOrPut(baseClass, ::hashMapOf)
198-
val previousSerializer = baseClassSerializers[concreteClass]
199197
val names = polyBase2NamedSerializers.getOrPut(baseClass, ::hashMapOf)
200-
if (allowOverwrite) {
201-
// Remove previous serializers from name mapping
202-
if (previousSerializer != null) {
203-
names.remove(previousSerializer.descriptor.serialName)
204-
}
205-
// Update mappings
206-
baseClassSerializers[concreteClass] = concreteSerializer
207-
names[name] = concreteSerializer
208-
return
209-
}
210-
// Overwrite prohibited
211-
if (previousSerializer != null) {
212-
if (previousSerializer != concreteSerializer) {
213-
throw SerializerAlreadyRegisteredException(baseClass, concreteClass)
214-
} else {
215-
// Cleanup name mapping
216-
names.remove(previousSerializer.descriptor.serialName)
217-
}
198+
199+
// Check KClass conflict
200+
val previousSerializer = baseClassSerializers[concreteClass]
201+
if (previousSerializer != null && previousSerializer != concreteSerializer) {
202+
if (allowOverwrite) names.remove(previousSerializer.descriptor.serialName)
203+
else throw SerializerAlreadyRegisteredException(baseClass, concreteClass)
218204
}
205+
206+
// Check SerialName conflict
219207
val previousByName = names[name]
220-
if (previousByName != null) {
221-
val conflictingClass = polyBase2Serializers[baseClass]!!.asSequence().find { it.value === previousByName }
222-
throw IllegalArgumentException(
223-
"Multiple polymorphic serializers for base class '$baseClass' " +
224-
"have the same serial name '$name': '$concreteClass' and '$conflictingClass'"
208+
if (previousByName != null && previousByName != concreteSerializer) {
209+
val previousClass = baseClassSerializers.asSequence().find { it.value === previousByName }?.key
210+
?: error("Name $name is registered in the module but no Kotlin class is associated with it.")
211+
212+
if (allowOverwrite) baseClassSerializers.remove(previousClass)
213+
else throw IllegalArgumentException(
214+
"Multiple polymorphic serializers in a scope of '$baseClass' " +
215+
"have the same serial name '$name': $concreteSerializer for '$concreteClass' and $previousByName for '$previousClass'"
225216
)
226217
}
227-
// Overwrite if no conflicts
218+
228219
baseClassSerializers[concreteClass] = concreteSerializer
229220
names[name] = concreteSerializer
230221
}

core/commonTest/src/kotlinx/serialization/modules/ModuleBuildersTest.kt

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@ import kotlinx.serialization.*
1010
import kotlinx.serialization.builtins.*
1111
import kotlinx.serialization.descriptors.*
1212
import kotlinx.serialization.encoding.*
13+
import kotlinx.serialization.test.Platform
14+
import kotlinx.serialization.test.assertFailsWithMessage
15+
import kotlinx.serialization.test.currentPlatform
16+
import kotlinx.serialization.test.isJs
17+
import kotlinx.serialization.test.isJvm
1318
import kotlin.reflect.*
1419
import kotlin.test.*
1520

@@ -176,6 +181,10 @@ class ModuleBuildersTest {
176181
@SerialName("C")
177182
class C
178183

184+
@Serializable
185+
@SerialName("C")
186+
class C2
187+
179188
@Serializer(forClass = C::class)
180189
object CSerializer : KSerializer<C> {
181190
override val descriptor: SerialDescriptor = buildSerialDescriptor("AnotherName", StructureKind.OBJECT)
@@ -206,6 +215,27 @@ class ModuleBuildersTest {
206215
assertNull(result.getPolymorphic(Any::class, serializedClassName = "AnotherName"))
207216
}
208217

218+
@Test
219+
fun testOverwriteWithDifferentClass() {
220+
val c1 = SerializersModule {
221+
polymorphic<Any>(Any::class) {
222+
subclass(C::class)
223+
}
224+
}
225+
val c2 = SerializersModule {
226+
polymorphic<Any>(Any::class) {
227+
subclass(C2::class)
228+
}
229+
}
230+
val classNameMsg = if (currentPlatform == Platform.JS || currentPlatform == Platform.WASM) "class Any" else "class kotlin.Any"
231+
assertFailsWithMessage<IllegalArgumentException>("Multiple polymorphic serializers in a scope of '$classNameMsg' have the same serial name 'C'") { c1 + c2 }
232+
val module = c1 overwriteWith c2
233+
// C should not be registered at all, C2 should be registered both under "C" and C2::class
234+
assertEquals(C2.serializer(), module.getPolymorphic(Any::class, serializedClassName = "C"))
235+
assertNull(module.getPolymorphic(Any::class, C()))
236+
assertEquals(C2.serializer(), module.getPolymorphic(Any::class, C2()))
237+
}
238+
209239
@Test
210240
fun testOverwriteWithSameSerialName() {
211241
val m1 = SerializersModule {

core/commonTest/src/kotlinx/serialization/test/TestHelpers.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,7 @@ inline fun jvmOnly(test: () -> Unit) {
3838
if (isJvm()) test()
3939
}
4040

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

0 commit comments

Comments
 (0)