From be7af57c7f433058a54255cb65b8801831dba6d5 Mon Sep 17 00:00:00 2001 From: Leonid Startsev Date: Tue, 25 May 2021 16:59:33 +0300 Subject: [PATCH] Introduce @InheritableSerialInfo and @JsonClassDiscriminator to configure discriminator per polymorphic base class Fixes #546 --- core/api/kotlinx-serialization-core.api | 6 + .../src/kotlinx/serialization/Annotations.kt | 38 ++++++ .../serialization/PolymorphicSerializer.kt | 21 ++-- .../kotlinx/serialization/SealedSerializer.kt | 35 ++++-- .../descriptors/SerialDescriptors.kt | 5 +- .../internal/ObjectSerializer.kt | 15 ++- .../PluginGeneratedSerialDescriptor.kt | 8 +- .../InheritableSerialInfoTest.kt | 48 ++++++++ .../SerialDescriptorAnnotationsTest.kt | 6 +- .../json/api/kotlinx-serialization-json.api | 9 ++ .../serialization/json/JsonAnnotations.kt | 72 ++++++++++++ .../kotlinx/serialization/json/JsonNames.kt | 39 ------- .../json/internal/Polymorphic.kt | 39 ++++--- .../json/internal/StreamingJsonEncoder.kt | 10 +- .../json/internal/TreeJsonEncoder.kt | 10 +- .../features/JsonClassDiscriminatorTest.kt | 110 ++++++++++++++++++ .../SerialNameCollisionInSealedClassesTest.kt | 4 +- .../json/internal/DynamicEncoders.kt | 10 +- .../json/DynamicPolymorphismTest.kt | 19 +++ 19 files changed, 398 insertions(+), 106 deletions(-) create mode 100644 core/commonTest/src/kotlinx/serialization/InheritableSerialInfoTest.kt create mode 100644 formats/json/commonMain/src/kotlinx/serialization/json/JsonAnnotations.kt delete mode 100644 formats/json/commonMain/src/kotlinx/serialization/json/JsonNames.kt create mode 100644 formats/json/commonTest/src/kotlinx/serialization/features/JsonClassDiscriminatorTest.kt diff --git a/core/api/kotlinx-serialization-core.api b/core/api/kotlinx-serialization-core.api index a962c01b6..4d07ae122 100644 --- a/core/api/kotlinx-serialization-core.api +++ b/core/api/kotlinx-serialization-core.api @@ -33,6 +33,9 @@ public final class kotlinx/serialization/EncodeDefault$Mode : java/lang/Enum { public abstract interface annotation class kotlinx/serialization/ExperimentalSerializationApi : java/lang/annotation/Annotation { } +public abstract interface annotation class kotlinx/serialization/InheritableSerialInfo : java/lang/annotation/Annotation { +} + public abstract interface annotation class kotlinx/serialization/InternalSerializationApi : java/lang/annotation/Annotation { } @@ -49,6 +52,7 @@ public abstract interface annotation class kotlinx/serialization/Polymorphic : j public final class kotlinx/serialization/PolymorphicSerializer : kotlinx/serialization/internal/AbstractPolymorphicSerializer { public fun (Lkotlin/reflect/KClass;)V + public fun (Lkotlin/reflect/KClass;[Ljava/lang/annotation/Annotation;)V public fun getBaseClass ()Lkotlin/reflect/KClass; public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; public fun toString ()Ljava/lang/String; @@ -64,6 +68,7 @@ public abstract interface annotation class kotlinx/serialization/Required : java public final class kotlinx/serialization/SealedClassSerializer : kotlinx/serialization/internal/AbstractPolymorphicSerializer { public fun (Ljava/lang/String;Lkotlin/reflect/KClass;[Lkotlin/reflect/KClass;[Lkotlinx/serialization/KSerializer;)V + public fun (Ljava/lang/String;Lkotlin/reflect/KClass;[Lkotlin/reflect/KClass;[Lkotlinx/serialization/KSerializer;[Ljava/lang/annotation/Annotation;)V public fun findPolymorphicSerializerOrNull (Lkotlinx/serialization/encoding/CompositeDecoder;Ljava/lang/String;)Lkotlinx/serialization/DeserializationStrategy; public fun findPolymorphicSerializerOrNull (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)Lkotlinx/serialization/SerializationStrategy; public fun getBaseClass ()Lkotlin/reflect/KClass; @@ -876,6 +881,7 @@ public final class kotlinx/serialization/internal/NullableSerializer : kotlinx/s public final class kotlinx/serialization/internal/ObjectSerializer : kotlinx/serialization/KSerializer { public fun (Ljava/lang/String;Ljava/lang/Object;)V + public fun (Ljava/lang/String;Ljava/lang/Object;[Ljava/lang/annotation/Annotation;)V public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; public fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V diff --git a/core/commonMain/src/kotlinx/serialization/Annotations.kt b/core/commonMain/src/kotlinx/serialization/Annotations.kt index bbbb6f2ad..b7710a90e 100644 --- a/core/commonMain/src/kotlinx/serialization/Annotations.kt +++ b/core/commonMain/src/kotlinx/serialization/Annotations.kt @@ -188,6 +188,44 @@ public annotation class EncodeDefault(val mode: Mode = Mode.ALWAYS) { @ExperimentalSerializationApi public annotation class SerialInfo +/** + * Meta-annotation that commands the compiler plugin to handle the annotation as serialization-specific. + * Serialization-specific annotations are preserved in the [SerialDescriptor] and can be retrieved + * during serialization process with [SerialDescriptor.getElementAnnotations]. + * + * In contrary to regular [SerialInfo], this one makes annotations inheritable: + * If class X marked as [Serializable] has any of its supertypes annotated with annotation A that has `@InheritableSerialInfo` on it, + * A appears in X's [SerialDescriptor] even if X itself is not annotated. + * It is possible to use A multiple times on different supertypes. Resulting X's [SerialDescriptor.annotations] would still contain + * only one instance of A. + * Note that if A has any arguments, their values should be the same across all hierarchy. Otherwise, a compilation error + * would be reported by the plugin. + * + * Example: + * ``` + * @InheritableSerialInfo + * annotation class A(val value: Int) + * + * @A(1) // Annotation can also be inherited from interfaces + * interface I + * + * @Serializable + * @A(1) // Argument value is the same as in I, no compiler error + * abstract class Base: I + * + * @Serializable + * class Derived: Base() + * + * // This function returns 1. + * fun foo(): Int = Derived.serializer().descriptor.annotations.filterIsInstance().single().value + * ``` + */ +@Target(AnnotationTarget.ANNOTATION_CLASS) +@Retention(AnnotationRetention.BINARY) +@ExperimentalSerializationApi +public annotation class InheritableSerialInfo + + /** * Instructs the plugin to use [ContextualSerializer] on a given property or type. * Context serializer is usually used when serializer for type can only be found in runtime. diff --git a/core/commonMain/src/kotlinx/serialization/PolymorphicSerializer.kt b/core/commonMain/src/kotlinx/serialization/PolymorphicSerializer.kt index 6c1dbad4a..311f809db 100644 --- a/core/commonMain/src/kotlinx/serialization/PolymorphicSerializer.kt +++ b/core/commonMain/src/kotlinx/serialization/PolymorphicSerializer.kt @@ -1,5 +1,5 @@ /* - * Copyright 2017-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2017-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ package kotlinx.serialization @@ -68,27 +68,26 @@ import kotlin.reflect.* @OptIn(ExperimentalSerializationApi::class) public class PolymorphicSerializer(override val baseClass: KClass) : AbstractPolymorphicSerializer() { - @PublishedApi // should we allow user access to this constructor? + @PublishedApi // See comment in SealedClassSerializer internal constructor( baseClass: KClass, classAnnotations: Array ) : this(baseClass) { - _descriptor.annotations = classAnnotations.asList() + _annotations = classAnnotations.asList() } - private val _descriptor: SerialDescriptorImpl = + private var _annotations: List = emptyList() + + public override val descriptor: SerialDescriptor by lazy(LazyThreadSafetyMode.PUBLICATION) { buildSerialDescriptor("kotlinx.serialization.Polymorphic", PolymorphicKind.OPEN) { element("type", String.serializer().descriptor) element( "value", - buildSerialDescriptor( - "kotlinx.serialization.Polymorphic<${baseClass.simpleName}>", - SerialKind.CONTEXTUAL - ) + buildSerialDescriptor("kotlinx.serialization.Polymorphic<${baseClass.simpleName}>", SerialKind.CONTEXTUAL) ) - } as SerialDescriptorImpl - - public override val descriptor: SerialDescriptor = _descriptor.withContext(baseClass) + annotations = _annotations + }.withContext(baseClass) + } override fun toString(): String { return "kotlinx.serialization.PolymorphicSerializer(baseClass: $baseClass)" diff --git a/core/commonMain/src/kotlinx/serialization/SealedSerializer.kt b/core/commonMain/src/kotlinx/serialization/SealedSerializer.kt index 132272934..354229ff9 100644 --- a/core/commonMain/src/kotlinx/serialization/SealedSerializer.kt +++ b/core/commonMain/src/kotlinx/serialization/SealedSerializer.kt @@ -77,6 +77,16 @@ public class SealedClassSerializer( subclassSerializers: Array> ) : AbstractPolymorphicSerializer() { + /** + * This constructor is needed to store serial info annotations defined on the sealed class. + * Support for such annotations was added in Kotlin 1.5.30; previous plugins used primary constructor of this class + * directly, therefore this constructor is secondary. + * + * This constructor can (and should) became primary when Require-Kotlin-Version is raised to at least 1.5.30 + * to remove necessity to store annotations separately and calculate descriptor via `lazy {}`. + * + * When doing this change, also migrate secondary constructors from [PolymorphicSerializer] and [ObjectSerializer]. + */ @PublishedApi internal constructor( serialName: String, @@ -85,19 +95,24 @@ public class SealedClassSerializer( subclassSerializers: Array>, classAnnotations: Array ) : this(serialName, baseClass, subclasses, subclassSerializers) { - (this.descriptor as SerialDescriptorImpl).annotations = classAnnotations.asList() + this._annotations = classAnnotations.asList() } - override val descriptor: SerialDescriptor = buildSerialDescriptor(serialName, PolymorphicKind.SEALED) { - element("type", String.serializer().descriptor) - val elementDescriptor = - buildSerialDescriptor("kotlinx.serialization.Sealed<${baseClass.simpleName}>", SerialKind.CONTEXTUAL) { - subclassSerializers.forEach { - val d = it.descriptor - element(d.serialName, d) + private var _annotations: List = emptyList() + + override val descriptor: SerialDescriptor by lazy(LazyThreadSafetyMode.PUBLICATION) { + buildSerialDescriptor(serialName, PolymorphicKind.SEALED) { + element("type", String.serializer().descriptor) + val elementDescriptor = + buildSerialDescriptor("kotlinx.serialization.Sealed<${baseClass.simpleName}>", SerialKind.CONTEXTUAL) { + subclassSerializers.forEach { + val d = it.descriptor + element(d.serialName, d) + } } - } - element("value", elementDescriptor) + element("value", elementDescriptor) + annotations = _annotations + } } private val class2Serializer: Map, KSerializer> diff --git a/core/commonMain/src/kotlinx/serialization/descriptors/SerialDescriptors.kt b/core/commonMain/src/kotlinx/serialization/descriptors/SerialDescriptors.kt index 4acd617e4..56d844543 100644 --- a/core/commonMain/src/kotlinx/serialization/descriptors/SerialDescriptors.kt +++ b/core/commonMain/src/kotlinx/serialization/descriptors/SerialDescriptors.kt @@ -1,5 +1,5 @@ /* - * Copyright 2017-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2017-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ package kotlinx.serialization.descriptors @@ -308,8 +308,7 @@ internal class SerialDescriptorImpl( builder: ClassSerialDescriptorBuilder ) : SerialDescriptor, CachedNames { - override var annotations: List = builder.annotations - internal set + override val annotations: List = builder.annotations override val serialNames: Set = builder.elementNames.toHashSet() private val elementNames: Array = builder.elementNames.toTypedArray() diff --git a/core/commonMain/src/kotlinx/serialization/internal/ObjectSerializer.kt b/core/commonMain/src/kotlinx/serialization/internal/ObjectSerializer.kt index 9542fd81b..450938aca 100644 --- a/core/commonMain/src/kotlinx/serialization/internal/ObjectSerializer.kt +++ b/core/commonMain/src/kotlinx/serialization/internal/ObjectSerializer.kt @@ -1,5 +1,5 @@ /* - * Copyright 2017-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2017-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ package kotlinx.serialization.internal @@ -17,16 +17,23 @@ import kotlinx.serialization.encoding.* @PublishedApi @OptIn(ExperimentalSerializationApi::class) internal class ObjectSerializer(serialName: String, private val objectInstance: T) : KSerializer { - @PublishedApi + + @PublishedApi // See comment in SealedClassSerializer internal constructor( serialName: String, objectInstance: T, classAnnotations: Array ) : this(serialName, objectInstance) { - (this.descriptor as SerialDescriptorImpl).annotations = classAnnotations.asList() + _annotations = classAnnotations.asList() } - override val descriptor: SerialDescriptor = buildSerialDescriptor(serialName, StructureKind.OBJECT) + private var _annotations: List = emptyList() + + override val descriptor: SerialDescriptor by lazy(LazyThreadSafetyMode.PUBLICATION) { + buildSerialDescriptor(serialName, StructureKind.OBJECT) { + annotations = _annotations + } + } override fun serialize(encoder: Encoder, value: T) { encoder.beginStructure(descriptor).endStructure(descriptor) diff --git a/core/commonMain/src/kotlinx/serialization/internal/PluginGeneratedSerialDescriptor.kt b/core/commonMain/src/kotlinx/serialization/internal/PluginGeneratedSerialDescriptor.kt index 278e6ed84..7b6efe7e2 100644 --- a/core/commonMain/src/kotlinx/serialization/internal/PluginGeneratedSerialDescriptor.kt +++ b/core/commonMain/src/kotlinx/serialization/internal/PluginGeneratedSerialDescriptor.kt @@ -1,5 +1,5 @@ /* - * Copyright 2017-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2017-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ @file:Suppress("OPTIONAL_DECLARATION_USAGE_IN_NON_COMMON_SOURCE", "UNUSED") @@ -33,15 +33,15 @@ internal open class PluginGeneratedSerialDescriptor( private var indices: Map = emptyMap() // Cache child serializers, they are not cached by the implementation for nullable types - private val childSerializers: Array> by lazy { generatedSerializer?.childSerializers() ?: emptyArray() } + private val childSerializers: Array> by lazy(LazyThreadSafetyMode.PUBLICATION) { generatedSerializer?.childSerializers() ?: EMPTY_SERIALIZER_ARRAY } // Lazy because of JS specific initialization order (#789) - internal val typeParameterDescriptors: Array by lazy { + internal val typeParameterDescriptors: Array by lazy(LazyThreadSafetyMode.PUBLICATION) { generatedSerializer?.typeParametersSerializers()?.map { it.descriptor }.compactArray() } // Can be without synchronization but Native will likely break due to freezing - private val _hashCode: Int by lazy { hashCodeImpl(typeParameterDescriptors) } + private val _hashCode: Int by lazy(LazyThreadSafetyMode.PUBLICATION) { hashCodeImpl(typeParameterDescriptors) } public fun addElement(name: String, isOptional: Boolean = false) { names[++added] = name diff --git a/core/commonTest/src/kotlinx/serialization/InheritableSerialInfoTest.kt b/core/commonTest/src/kotlinx/serialization/InheritableSerialInfoTest.kt new file mode 100644 index 000000000..a117ad484 --- /dev/null +++ b/core/commonTest/src/kotlinx/serialization/InheritableSerialInfoTest.kt @@ -0,0 +1,48 @@ +package kotlinx.serialization + +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.test.isJsLegacy +import kotlin.test.* + + +class InheritableSerialInfoTest { + + @InheritableSerialInfo + annotation class InheritableDiscriminator(val discriminator: String) + + @InheritableDiscriminator("a") + interface A + + @InheritableDiscriminator("a") + interface B + + @InheritableDiscriminator("a") + @Serializable + abstract class C: A + + @Serializable + sealed class D: C(), B + + @Serializable + class E: D() + + @Serializable + class E2: C() + + @Serializable + class E3: A, B + + private fun doTest(descriptor: SerialDescriptor) { + if (isJsLegacy()) return // Unsupported + val list = descriptor.annotations.filterIsInstance() + assertEquals(1, list.size) + assertEquals("a", list.first().discriminator) + } + + @Test + fun testInheritanceFromSealed() = doTest(E.serializer().descriptor) + @Test + fun testInheritanceFromAbstract() = doTest(E2.serializer().descriptor) + @Test + fun testInheritanceFromInterface() = doTest(E3.serializer().descriptor) +} diff --git a/core/commonTest/src/kotlinx/serialization/SerialDescriptorAnnotationsTest.kt b/core/commonTest/src/kotlinx/serialization/SerialDescriptorAnnotationsTest.kt index de25da464..f2010a2d9 100644 --- a/core/commonTest/src/kotlinx/serialization/SerialDescriptorAnnotationsTest.kt +++ b/core/commonTest/src/kotlinx/serialization/SerialDescriptorAnnotationsTest.kt @@ -5,6 +5,7 @@ package kotlinx.serialization import kotlinx.serialization.descriptors.* +import kotlinx.serialization.test.isJsLegacy import kotlin.test.* class SerialDescriptorAnnotationsTest { @@ -23,7 +24,7 @@ class SerialDescriptorAnnotationsTest { @SerialInfo @Target(AnnotationTarget.PROPERTY, AnnotationTarget.CLASS) - annotation class CustomAnnotationWithDefault(val value: String = "foo") + annotation class CustomAnnotationWithDefault(val value: String = "default_annotation_value") @SerialInfo @Target(AnnotationTarget.PROPERTY) @@ -70,7 +71,7 @@ class SerialDescriptorAnnotationsTest { val value = WithNames.serializer().descriptor .getElementAnnotations(1).filterIsInstance().single() - assertEquals("foo", value.value) + assertEquals("default_annotation_value", value.value) } @Test @@ -102,6 +103,7 @@ class SerialDescriptorAnnotationsTest { class Holder(val r: Result, val a: AbstractResult, val o: ObjectResult, @Contextual val names: WithNames) private fun doTest(position: Int, expected: String) { + if (isJsLegacy()) return // Unsupported val desc = Holder.serializer().descriptor.getElementDescriptor(position) assertEquals(expected, desc.annotations.getCustom()) } diff --git a/formats/json/api/kotlinx-serialization-json.api b/formats/json/api/kotlinx-serialization-json.api index cc9672c6d..490097087 100644 --- a/formats/json/api/kotlinx-serialization-json.api +++ b/formats/json/api/kotlinx-serialization-json.api @@ -103,6 +103,15 @@ public final class kotlinx/serialization/json/JsonBuilder { public final fun setUseArrayPolymorphism (Z)V } +public abstract interface annotation class kotlinx/serialization/json/JsonClassDiscriminator : java/lang/annotation/Annotation { + public abstract fun discriminator ()Ljava/lang/String; +} + +public final class kotlinx/serialization/json/JsonClassDiscriminator$Impl : kotlinx/serialization/json/JsonClassDiscriminator { + public fun (Ljava/lang/String;)V + public final synthetic fun discriminator ()Ljava/lang/String; +} + public final class kotlinx/serialization/json/JsonConfiguration { public fun ()V public final fun getAllowSpecialFloatingPointValues ()Z diff --git a/formats/json/commonMain/src/kotlinx/serialization/json/JsonAnnotations.kt b/formats/json/commonMain/src/kotlinx/serialization/json/JsonAnnotations.kt new file mode 100644 index 000000000..aae69889a --- /dev/null +++ b/formats/json/commonMain/src/kotlinx/serialization/json/JsonAnnotations.kt @@ -0,0 +1,72 @@ +/* + * Copyright 2017-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.serialization.json + +import kotlinx.serialization.* +import kotlinx.serialization.descriptors.* +import kotlinx.serialization.encoding.* +import kotlinx.serialization.json.internal.* +import kotlin.native.concurrent.* + +/** + * Indicates that the field can be represented in JSON + * with multiple possible alternative names. + * [Json] format recognizes this annotation and is able to decode + * the data using any of the alternative names. + * + * Unlike [SerialName] annotation, does not affect JSON encoding in any way. + * + * Example of usage: + * ``` + * @Serializable + * data class Project(@JsonNames("title") val name: String) + * + * val project = Json.decodeFromString("""{"name":"kotlinx.serialization"}""") + * println(project) + * val oldProject = Json.decodeFromString("""{"title":"kotlinx.coroutines"}""") + * println(oldProject) + * ``` + * + * This annotation has lesser priority than [SerialName]. + * + * @see JsonBuilder.useAlternativeNames + */ +@SerialInfo +@Target(AnnotationTarget.PROPERTY) +@ExperimentalSerializationApi +public annotation class JsonNames(vararg val names: String) + +/** + * Specifies key for class discriminator value used during polymorphic serialization in [Json]. + * Provided key is used only for an annotated class and its subclasses; + * to configure global class discriminator, use [JsonBuilder.classDiscriminator] + * property. + * + * This annotation is [inheritable][InheritableSerialInfo], so it should be sufficient to place it on a base class of hierarchy. + * It is not possible to define different class discriminators for different parts of class hierarchy. + * Pay attention to the fact that class discriminator, same as polymorphic serializer's base class, is + * determined statically. + * + * Example: + * ``` + * @Serializable + * @JsonClassDiscriminator("message_type") + * abstract class Base + * + * @Serializable // Class discriminator is inherited from Base + * abstract class ErrorClass: Base() + * + * @Serializable + * class Message(val message: Base, val error: ErrorClass?) + * + * val message = Json.decodeFromString("""{"message": {"message_type":"my.app.BaseMessage", "message": "not found"}, "error": {"message_type":"my.app.GenericError", "error_code": 404}}""") + * ``` + * + * @see JsonBuilder.classDiscriminator + */ +@InheritableSerialInfo +@Target(AnnotationTarget.CLASS) +@ExperimentalSerializationApi +public annotation class JsonClassDiscriminator(val discriminator: String) diff --git a/formats/json/commonMain/src/kotlinx/serialization/json/JsonNames.kt b/formats/json/commonMain/src/kotlinx/serialization/json/JsonNames.kt deleted file mode 100644 index fcb122780..000000000 --- a/formats/json/commonMain/src/kotlinx/serialization/json/JsonNames.kt +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright 2017-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.serialization.json - -import kotlinx.serialization.* -import kotlinx.serialization.descriptors.* -import kotlinx.serialization.encoding.* -import kotlinx.serialization.json.internal.* -import kotlin.native.concurrent.* - -/** - * An annotation that indicates the field can be represented in JSON - * with multiple possible alternative names. - * [Json] format recognizes this annotation and is able to decode - * the data using any of the alternative names. - * - * Unlike [SerialName] annotation, does not affect JSON encoding in any way. - * - * Example of usage: - * ``` - * @Serializable - * data class Project(@JsonNames("title") val name: String) - * - * val project = Json.decodeFromString("""{"name":"kotlinx.serialization"}""") - * println(project) - * val oldProject = Json.decodeFromString("""{"title":"kotlinx.coroutines"}""") - * println(oldProject) - * ``` - * - * This annotation has lesser priority than [SerialName]. - * - * @see JsonBuilder.useAlternativeNames - */ -@SerialInfo -@Target(AnnotationTarget.PROPERTY) -@ExperimentalSerializationApi -public annotation class JsonNames(vararg val names: String) diff --git a/formats/json/commonMain/src/kotlinx/serialization/json/internal/Polymorphic.kt b/formats/json/commonMain/src/kotlinx/serialization/json/internal/Polymorphic.kt index 73f00c476..ea65c48ac 100644 --- a/formats/json/commonMain/src/kotlinx/serialization/json/internal/Polymorphic.kt +++ b/formats/json/commonMain/src/kotlinx/serialization/json/internal/Polymorphic.kt @@ -11,26 +11,22 @@ import kotlinx.serialization.internal.* import kotlinx.serialization.json.* @Suppress("UNCHECKED_CAST") -internal inline fun JsonEncoder.encodePolymorphically(serializer: SerializationStrategy, value: T, ifPolymorphic: () -> Unit) { +internal inline fun JsonEncoder.encodePolymorphically( + serializer: SerializationStrategy, + value: T, + ifPolymorphic: (String) -> Unit +) { if (serializer !is AbstractPolymorphicSerializer<*> || json.configuration.useArrayPolymorphism) { serializer.serialize(this, value) return } - val actualSerializer = findActualSerializer(serializer as SerializationStrategy, value as Any) - ifPolymorphic() - actualSerializer.serialize(this, value) -} - -private fun JsonEncoder.findActualSerializer( - serializer: SerializationStrategy, - value: Any -): SerializationStrategy { val casted = serializer as AbstractPolymorphicSerializer - val actualSerializer = casted.findPolymorphicSerializer(this, value) - validateIfSealed(casted, actualSerializer, json.configuration.classDiscriminator) - val kind = actualSerializer.descriptor.kind - checkKind(kind) - return actualSerializer + val baseClassDiscriminator = serializer.descriptor.classDiscriminator(json) + val actualSerializer = casted.findPolymorphicSerializer(this, value as Any) + validateIfSealed(casted, actualSerializer, baseClassDiscriminator) + checkKind(actualSerializer.descriptor.kind) + ifPolymorphic(baseClassDiscriminator) + actualSerializer.serialize(this, value) } private fun validateIfSealed( @@ -64,7 +60,7 @@ internal fun JsonDecoder.decodeSerializableValuePolymorphic(deserializer: De } val jsonTree = cast(decodeJsonElement(), deserializer.descriptor) - val discriminator = json.configuration.classDiscriminator + val discriminator = deserializer.descriptor.classDiscriminator(json) val type = jsonTree[discriminator]?.jsonPrimitive?.content val actualSerializer = deserializer.findPolymorphicSerializerOrNull(this, type) ?: throwSerializerNotFound(type, jsonTree) @@ -79,3 +75,14 @@ private fun throwSerializerNotFound(type: String?, jsonTree: JsonObject): Nothin else "class discriminator '$type'" throw JsonDecodingException(-1, "Polymorphic serializer was not found for $suffix", jsonTree.toString()) } + +internal fun SerialDescriptor.classDiscriminator(json: Json): String { + // Plain loop is faster than allocation of Sequence or ArrayList + // We can rely on the fact that only one JsonClassDiscriminator is present — + // compiler plugin checked that. + for (annotation in annotations) { + if (annotation is JsonClassDiscriminator) return annotation.discriminator + } + return json.configuration.classDiscriminator +} + diff --git a/formats/json/commonMain/src/kotlinx/serialization/json/internal/StreamingJsonEncoder.kt b/formats/json/commonMain/src/kotlinx/serialization/json/internal/StreamingJsonEncoder.kt index 2693e8e29..59a776503 100644 --- a/formats/json/commonMain/src/kotlinx/serialization/json/internal/StreamingJsonEncoder.kt +++ b/formats/json/commonMain/src/kotlinx/serialization/json/internal/StreamingJsonEncoder.kt @@ -44,7 +44,7 @@ internal class StreamingJsonEncoder( // Forces serializer to wrap all values into quotes private var forceQuoting: Boolean = false - private var writePolymorphic = false + private var polymorphicDiscriminator: String? = null init { val i = mode.ordinal @@ -64,13 +64,13 @@ internal class StreamingJsonEncoder( override fun encodeSerializableValue(serializer: SerializationStrategy, value: T) { encodePolymorphically(serializer, value) { - writePolymorphic = true + polymorphicDiscriminator = it } } private fun encodeTypeInfo(descriptor: SerialDescriptor) { composer.nextItem() - encodeString(configuration.classDiscriminator) + encodeString(polymorphicDiscriminator!!) composer.print(COLON) composer.space() encodeString(descriptor.serialName) @@ -83,9 +83,9 @@ internal class StreamingJsonEncoder( composer.indent() } - if (writePolymorphic) { - writePolymorphic = false + if (polymorphicDiscriminator != null) { encodeTypeInfo(descriptor) + polymorphicDiscriminator = null } if (mode == newMode) { diff --git a/formats/json/commonMain/src/kotlinx/serialization/json/internal/TreeJsonEncoder.kt b/formats/json/commonMain/src/kotlinx/serialization/json/internal/TreeJsonEncoder.kt index a8df371fb..f89f4da49 100644 --- a/formats/json/commonMain/src/kotlinx/serialization/json/internal/TreeJsonEncoder.kt +++ b/formats/json/commonMain/src/kotlinx/serialization/json/internal/TreeJsonEncoder.kt @@ -33,7 +33,7 @@ private sealed class AbstractJsonTreeEncoder( @JvmField protected val configuration = json.configuration - private var writePolymorphic = false + private var polymorphicDiscriminator: String? = null override fun encodeJsonElement(element: JsonElement) { encodeSerializableValue(JsonElementSerializer, element) @@ -70,7 +70,7 @@ private sealed class AbstractJsonTreeEncoder( override fun encodeSerializableValue(serializer: SerializationStrategy, value: T) { // Writing non-structured data (i.e. primitives) on top-level (e.g. without any tag) requires special output if (currentTagOrNull != null || serializer.descriptor.kind !is PrimitiveKind && serializer.descriptor.kind !== SerialKind.ENUM) { - encodePolymorphically(serializer, value) { writePolymorphic = true } + encodePolymorphically(serializer, value) { polymorphicDiscriminator = it } } else JsonPrimitiveEncoder(json, nodeConsumer).apply { encodeSerializableValue(serializer, value) endEncode(serializer.descriptor) @@ -126,9 +126,9 @@ private sealed class AbstractJsonTreeEncoder( else -> JsonTreeEncoder(json, consumer) } - if (writePolymorphic) { - writePolymorphic = false - encoder.putElement(configuration.classDiscriminator, JsonPrimitive(descriptor.serialName)) + if (polymorphicDiscriminator != null) { + encoder.putElement(polymorphicDiscriminator!!, JsonPrimitive(descriptor.serialName)) + polymorphicDiscriminator = null } return encoder diff --git a/formats/json/commonTest/src/kotlinx/serialization/features/JsonClassDiscriminatorTest.kt b/formats/json/commonTest/src/kotlinx/serialization/features/JsonClassDiscriminatorTest.kt new file mode 100644 index 000000000..93719c316 --- /dev/null +++ b/formats/json/commonTest/src/kotlinx/serialization/features/JsonClassDiscriminatorTest.kt @@ -0,0 +1,110 @@ +/* + * Copyright 2017-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.serialization.features + +import kotlinx.serialization.* +import kotlinx.serialization.builtins.* +import kotlinx.serialization.json.* +import kotlinx.serialization.modules.* +import kotlinx.serialization.test.noLegacyJs +import kotlin.test.* + +class JsonClassDiscriminatorTest : JsonTestBase() { + @Serializable + @JsonClassDiscriminator("sealedType") + sealed class SealedMessage { + @Serializable + @SerialName("SealedMessage.StringMessage") + data class StringMessage(val description: String, val message: String) : SealedMessage() + + @SerialName("EOF") + @Serializable + object EOF : SealedMessage() + } + + @Serializable + @JsonClassDiscriminator("abstractType") + abstract class AbstractMessage { + @Serializable + @SerialName("Message.StringMessage") + data class StringMessage(val description: String, val message: String) : AbstractMessage() + + @Serializable + @SerialName("Message.IntMessage") + data class IntMessage(val description: String, val message: Int) : AbstractMessage() + } + + + @Test + fun testSealedClassesHaveCustomDiscriminator() = noLegacyJs { + val messages = listOf( + SealedMessage.StringMessage("string message", "foo"), + SealedMessage.EOF + ) + val expected = + """[{"sealedType":"SealedMessage.StringMessage","description":"string message","message":"foo"},{"sealedType":"EOF"}]""" + assertJsonFormAndRestored( + ListSerializer(SealedMessage.serializer()), + messages, + expected, + ) + } + + @Test + fun testAbstractClassesHaveCustomDiscriminator() = noLegacyJs { + val messages = listOf( + AbstractMessage.StringMessage("string message", "foo"), + AbstractMessage.IntMessage("int message", 42), + ) + val module = SerializersModule { + polymorphic(AbstractMessage::class) { + subclass(AbstractMessage.StringMessage.serializer()) + subclass(AbstractMessage.IntMessage.serializer()) + } + } + val json = Json { serializersModule = module } + val expected = + """[{"abstractType":"Message.StringMessage","description":"string message","message":"foo"},{"abstractType":"Message.IntMessage","description":"int message","message":42}]""" + assertJsonFormAndRestored(ListSerializer(AbstractMessage.serializer()), messages, expected, json) + } + + @Serializable + @JsonClassDiscriminator("message_type") + abstract class Base + + @Serializable + abstract class ErrorClass : Base() + + @Serializable + data class Message(val message: Base, val error: ErrorClass?) + + @Serializable + @SerialName("my.app.BaseMessage") + data class BaseMessage(val message: String) : Base() + + @Serializable + @SerialName("my.app.GenericError") + data class GenericError(@SerialName("error_code") val errorCode: Int) : ErrorClass() + + + @Test + fun testDocumentationInheritanceSample() = noLegacyJs { + val module = SerializersModule { + polymorphic(Base::class) { + subclass(BaseMessage.serializer()) + } + polymorphic(ErrorClass::class) { + subclass(GenericError.serializer()) + } + } + val json = Json { serializersModule = module } + assertJsonFormAndRestored( + Message.serializer(), + Message(BaseMessage("not found"), GenericError(404)), + """{"message":{"message_type":"my.app.BaseMessage","message":"not found"},"error":{"message_type":"my.app.GenericError","error_code":404}}""", + json + ) + } +} diff --git a/formats/json/commonTest/src/kotlinx/serialization/modules/SerialNameCollisionInSealedClassesTest.kt b/formats/json/commonTest/src/kotlinx/serialization/modules/SerialNameCollisionInSealedClassesTest.kt index 31abe79e7..18a49a525 100644 --- a/formats/json/commonTest/src/kotlinx/serialization/modules/SerialNameCollisionInSealedClassesTest.kt +++ b/formats/json/commonTest/src/kotlinx/serialization/modules/SerialNameCollisionInSealedClassesTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2017-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2017-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ package kotlinx.serialization.modules @@ -49,6 +49,6 @@ class SerialNameCollisionInSealedClassesTest { BaseCollision.Child() BaseCollision.ChildCollided() BaseCollision.ChildCollided.serializer().descriptor // Doesn't fail - assertFailsWith { BaseCollision.serializer().descriptor } + assertFailsWith { BaseCollision.serializer().descriptor } } } diff --git a/formats/json/jsMain/src/kotlinx/serialization/json/internal/DynamicEncoders.kt b/formats/json/jsMain/src/kotlinx/serialization/json/internal/DynamicEncoders.kt index b97858e33..4bd46d59c 100644 --- a/formats/json/jsMain/src/kotlinx/serialization/json/internal/DynamicEncoders.kt +++ b/formats/json/jsMain/src/kotlinx/serialization/json/internal/DynamicEncoders.kt @@ -61,7 +61,7 @@ private class DynamicObjectEncoder( /** * Flag of usage polymorphism with discriminator attribute */ - private var writePolymorphic = false + private var polymorphicDiscriminator: String? = null private object NoOutputMark @@ -184,7 +184,7 @@ private class DynamicObjectEncoder( override fun encodeSerializableValue(serializer: SerializationStrategy, value: T) { encodePolymorphically(serializer, value) { - writePolymorphic = true + polymorphicDiscriminator = it } } @@ -208,9 +208,9 @@ private class DynamicObjectEncoder( enterNode(child, newMode) } - if (writePolymorphic) { - writePolymorphic = false - current.jsObject[json.configuration.classDiscriminator] = descriptor.serialName + if (polymorphicDiscriminator != null) { + current.jsObject[polymorphicDiscriminator!!] = descriptor.serialName + polymorphicDiscriminator = null } current.index = 0 diff --git a/formats/json/jsTest/src/kotlinx/serialization/json/DynamicPolymorphismTest.kt b/formats/json/jsTest/src/kotlinx/serialization/json/DynamicPolymorphismTest.kt index c95799a9b..0af00c667 100644 --- a/formats/json/jsTest/src/kotlinx/serialization/json/DynamicPolymorphismTest.kt +++ b/formats/json/jsTest/src/kotlinx/serialization/json/DynamicPolymorphismTest.kt @@ -8,6 +8,7 @@ import kotlinx.serialization.* import kotlinx.serialization.modules.SerializersModule import kotlinx.serialization.modules.polymorphic import kotlinx.serialization.modules.subclass +import kotlinx.serialization.test.noLegacyJs import kotlin.test.Test import kotlin.test.assertEquals @@ -39,6 +40,14 @@ class DynamicPolymorphismTest { data class DefaultChild(val default: String? = "default"): Sealed(5) } + @Serializable + @JsonClassDiscriminator("sealed_custom") + sealed class SealedCustom { + @Serializable + @SerialName("data_class") + data class DataClassChild(val name: String) : SealedCustom() + } + @Serializable data class CompositeClass(val mark: String, val nested: Sealed) @@ -75,6 +84,16 @@ class DynamicPolymorphismTest { } } + @Test + fun testCustomClassDiscriminator() = noLegacyJs { + val value = SealedCustom.DataClassChild("custom-discriminator-test") + encodeAndDecode(SealedCustom.serializer(), value, objectJson) { + assertEquals("data_class", this["sealed_custom"]) + assertEquals(undefined, this.type) + assertEquals(2, fieldsCount(this)) + } + } + @Test fun testComposite() { val nestedValue = Sealed.DataClassChild("child")