Skip to content

Commit e147b02

Browse files
authored
Do not check kind or discriminator collisions (#2833)
for subclasses' polymorphic serializers if Json.classDiscriminatorMode is set to NONE. This should simplify the code in situations where JSON is expected to be only sent and not parsed. Fixes #2753 Fixes #1486
1 parent 3c7fb8f commit e147b02

File tree

5 files changed

+67
-7
lines changed

5 files changed

+67
-7
lines changed

formats/json-tests/commonTest/src/kotlinx/serialization/json/polymorphic/JsonClassDiscriminatorModeTest.kt

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44

55
package kotlinx.serialization.json.polymorphic
66

7+
import kotlinx.serialization.*
78
import kotlinx.serialization.json.*
9+
import kotlinx.serialization.modules.*
810
import kotlin.test.*
911

1012
class ClassDiscriminatorModeAllObjectsTest :
@@ -80,5 +82,44 @@ class ClassDiscriminatorModeNoneTest :
8082

8183
@Test
8284
fun testNullable() = testNullable("""{"sb":null,"sc":null}""")
85+
86+
interface CommandType
87+
88+
@Serializable // For Kotlin/JS
89+
enum class Modify : CommandType {
90+
CREATE, DELETE
91+
}
92+
93+
@Serializable
94+
class Command(val cmd: CommandType)
95+
96+
@Test
97+
fun testNoneModeAllowsPolymorphicEnums() {
98+
val module = SerializersModule {
99+
polymorphic(CommandType::class) {
100+
subclass(Modify::class, Modify.serializer())
101+
}
102+
}
103+
val j = Json(default) { serializersModule = module; classDiscriminatorMode = ClassDiscriminatorMode.NONE }
104+
parametrizedTest { mode ->
105+
assertEquals("""{"cmd":"CREATE"}""", j.encodeToString(Command(Modify.CREATE), mode))
106+
}
107+
}
108+
109+
@Serializable
110+
class SomeCommand(val type: String) : CommandType
111+
112+
@Test
113+
fun testNoneModeAllowsDiscriminatorClash() {
114+
val module = SerializersModule {
115+
polymorphic(CommandType::class) {
116+
subclass(SomeCommand::class)
117+
}
118+
}
119+
val j = Json(default) { serializersModule = module; classDiscriminatorMode = ClassDiscriminatorMode.NONE }
120+
parametrizedTest { mode ->
121+
assertEquals("""{"cmd":{"type":"foo"}}""", j.encodeToString(Command(SomeCommand("foo")), mode))
122+
}
123+
}
83124
}
84125

formats/json/commonMain/src/kotlinx/serialization/json/Json.kt

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -495,6 +495,13 @@ public class JsonBuilder internal constructor(json: Json) {
495495
/**
496496
* Name of the class descriptor property for polymorphic serialization.
497497
* `type` by default.
498+
*
499+
* Note that if your class has any serial names that are equal to [classDiscriminator]
500+
* (e.g., `@Serializable class Foo(val type: String)`), an [IllegalArgumentException] will be thrown from `Json {}` builder.
501+
* You can disable this check and class discriminator inclusion with [ClassDiscriminatorMode.NONE], but kotlinx.serialization will not be
502+
* able to deserialize such data back.
503+
*
504+
* @see classDiscriminatorMode
498505
*/
499506
public var classDiscriminator: String = json.configuration.classDiscriminator
500507

@@ -504,6 +511,8 @@ public class JsonBuilder internal constructor(json: Json) {
504511
*
505512
* Other modes are generally intended to produce JSON for consumption by third-party libraries,
506513
* therefore, this setting does not affect the deserialization process.
514+
*
515+
* @see classDiscriminator
507516
*/
508517
@ExperimentalSerializationApi
509518
public var classDiscriminatorMode: ClassDiscriminatorMode = json.configuration.classDiscriminatorMode
@@ -669,7 +678,7 @@ private class JsonImpl(configuration: JsonConfiguration, module: SerializersModu
669678

670679
private fun validateConfiguration() {
671680
if (serializersModule == EmptySerializersModule()) return // Fast-path for in-place JSON allocations
672-
val collector = PolymorphismValidator(configuration.useArrayPolymorphism, configuration.classDiscriminator)
681+
val collector = JsonSerializersModuleValidator(configuration)
673682
serializersModule.dumpTo(collector)
674683
}
675684
}

formats/json/commonMain/src/kotlinx/serialization/json/JsonConfiguration.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,9 @@ public enum class ClassDiscriminatorMode {
8181
* This mode is generally intended to produce JSON for consumption by third-party libraries.
8282
* kotlinx.serialization is unable to deserialize [polymorphic classes][POLYMORPHIC] without class discriminators,
8383
* so it is impossible to deserialize JSON produced in this mode if a data model has polymorphic classes.
84+
*
85+
* Using this mode relaxes several configuration checks in [Json]. In particular, it is possible to serialize enums and primitives
86+
* as polymorphic subclasses in this mode, since it is no longer required for them to have outer `{}` object to include class discriminator.
8487
*/
8588
NONE,
8689

formats/json/commonMain/src/kotlinx/serialization/json/internal/PolymorphismValidator.kt renamed to formats/json/commonMain/src/kotlinx/serialization/json/internal/JsonSerializersModuleValidator.kt

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,19 @@ package kotlinx.serialization.json.internal
66

77
import kotlinx.serialization.*
88
import kotlinx.serialization.descriptors.*
9+
import kotlinx.serialization.json.*
910
import kotlinx.serialization.modules.*
1011
import kotlin.reflect.*
1112

1213
@OptIn(ExperimentalSerializationApi::class)
13-
internal class PolymorphismValidator(
14-
private val useArrayPolymorphism: Boolean,
15-
private val discriminator: String
14+
internal class JsonSerializersModuleValidator(
15+
configuration: JsonConfiguration,
1616
) : SerializersModuleCollector {
1717

18+
private val discriminator: String = configuration.classDiscriminator
19+
private val useArrayPolymorphism: Boolean = configuration.useArrayPolymorphism
20+
private val isDiscriminatorRequired = configuration.classDiscriminatorMode != ClassDiscriminatorMode.NONE
21+
1822
override fun <T : Any> contextual(
1923
kClass: KClass<T>,
2024
provider: (typeArgumentsSerializers: List<KSerializer<*>>) -> KSerializer<*>
@@ -29,7 +33,7 @@ internal class PolymorphismValidator(
2933
) {
3034
val descriptor = actualSerializer.descriptor
3135
checkKind(descriptor, actualClass)
32-
if (!useArrayPolymorphism) {
36+
if (!useArrayPolymorphism && isDiscriminatorRequired) {
3337
// Collisions with "type" can happen only for JSON polymorphism
3438
checkDiscriminatorCollisions(descriptor, actualClass)
3539
}
@@ -43,6 +47,7 @@ internal class PolymorphismValidator(
4347
}
4448

4549
if (useArrayPolymorphism) return
50+
if (!isDiscriminatorRequired) return
4651
/*
4752
* For this kind we can't intercept the JSON object {} in order to add "type: ...".
4853
* Except for maps that just can clash and accidentally overwrite the type.

formats/json/commonMain/src/kotlinx/serialization/json/internal/Polymorphic.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,10 @@ internal inline fun <T> JsonEncoder.encodePolymorphically(
3737
val casted = serializer as AbstractPolymorphicSerializer<Any>
3838
requireNotNull(value) { "Value for serializer ${serializer.descriptor} should always be non-null. Please report issue to the kotlinx.serialization tracker." }
3939
val actual = casted.findPolymorphicSerializer(this, value)
40-
if (baseClassDiscriminator != null) validateIfSealed(serializer, actual, baseClassDiscriminator)
41-
checkKind(actual.descriptor.kind)
40+
if (baseClassDiscriminator != null) {
41+
validateIfSealed(serializer, actual, baseClassDiscriminator)
42+
checkKind(actual.descriptor.kind)
43+
}
4244
actual as SerializationStrategy<T>
4345
} else serializer
4446

0 commit comments

Comments
 (0)