Skip to content

Commit

Permalink
Introduce @InheritableSerialInfo and @JsonClassDiscriminator to confi…
Browse files Browse the repository at this point in the history
…gure discriminator per polymorphic base class

Fixes #546
  • Loading branch information
sandwwraith committed Aug 26, 2021
1 parent 63eff4d commit be7af57
Show file tree
Hide file tree
Showing 19 changed files with 398 additions and 106 deletions.
6 changes: 6 additions & 0 deletions core/api/kotlinx-serialization-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
}

Expand All @@ -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 <init> (Lkotlin/reflect/KClass;)V
public fun <init> (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;
Expand All @@ -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 <init> (Ljava/lang/String;Lkotlin/reflect/KClass;[Lkotlin/reflect/KClass;[Lkotlinx/serialization/KSerializer;)V
public fun <init> (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;
Expand Down Expand Up @@ -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 <init> (Ljava/lang/String;Ljava/lang/Object;)V
public fun <init> (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
Expand Down
38 changes: 38 additions & 0 deletions core/commonMain/src/kotlinx/serialization/Annotations.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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<A>().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.
Expand Down
21 changes: 10 additions & 11 deletions core/commonMain/src/kotlinx/serialization/PolymorphicSerializer.kt
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -68,27 +68,26 @@ import kotlin.reflect.*
@OptIn(ExperimentalSerializationApi::class)
public class PolymorphicSerializer<T : Any>(override val baseClass: KClass<T>) : AbstractPolymorphicSerializer<T>() {

@PublishedApi // should we allow user access to this constructor?
@PublishedApi // See comment in SealedClassSerializer
internal constructor(
baseClass: KClass<T>,
classAnnotations: Array<Annotation>
) : this(baseClass) {
_descriptor.annotations = classAnnotations.asList()
_annotations = classAnnotations.asList()
}

private val _descriptor: SerialDescriptorImpl =
private var _annotations: List<Annotation> = 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)"
Expand Down
35 changes: 25 additions & 10 deletions core/commonMain/src/kotlinx/serialization/SealedSerializer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,16 @@ public class SealedClassSerializer<T : Any>(
subclassSerializers: Array<KSerializer<out T>>
) : AbstractPolymorphicSerializer<T>() {

/**
* 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,
Expand All @@ -85,19 +95,24 @@ public class SealedClassSerializer<T : Any>(
subclassSerializers: Array<KSerializer<out T>>,
classAnnotations: Array<Annotation>
) : 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<Annotation> = 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<KClass<out T>, KSerializer<out T>>
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -308,8 +308,7 @@ internal class SerialDescriptorImpl(
builder: ClassSerialDescriptorBuilder
) : SerialDescriptor, CachedNames {

override var annotations: List<Annotation> = builder.annotations
internal set
override val annotations: List<Annotation> = builder.annotations
override val serialNames: Set<String> = builder.elementNames.toHashSet()

private val elementNames: Array<String> = builder.elementNames.toTypedArray()
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -17,16 +17,23 @@ import kotlinx.serialization.encoding.*
@PublishedApi
@OptIn(ExperimentalSerializationApi::class)
internal class ObjectSerializer<T : Any>(serialName: String, private val objectInstance: T) : KSerializer<T> {
@PublishedApi

@PublishedApi // See comment in SealedClassSerializer
internal constructor(
serialName: String,
objectInstance: T,
classAnnotations: Array<Annotation>
) : 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<Annotation> = 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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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")

Expand Down Expand Up @@ -33,15 +33,15 @@ internal open class PluginGeneratedSerialDescriptor(

private var indices: Map<String, Int> = emptyMap()
// Cache child serializers, they are not cached by the implementation for nullable types
private val childSerializers: Array<KSerializer<*>> by lazy { generatedSerializer?.childSerializers() ?: emptyArray() }
private val childSerializers: Array<KSerializer<*>> by lazy(LazyThreadSafetyMode.PUBLICATION) { generatedSerializer?.childSerializers() ?: EMPTY_SERIALIZER_ARRAY }

// Lazy because of JS specific initialization order (#789)
internal val typeParameterDescriptors: Array<SerialDescriptor> by lazy {
internal val typeParameterDescriptors: Array<SerialDescriptor> 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
Expand Down
Original file line number Diff line number Diff line change
@@ -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<InheritableDiscriminator>()
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)
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
package kotlinx.serialization

import kotlinx.serialization.descriptors.*
import kotlinx.serialization.test.isJsLegacy
import kotlin.test.*

class SerialDescriptorAnnotationsTest {
Expand All @@ -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)
Expand Down Expand Up @@ -70,7 +71,7 @@ class SerialDescriptorAnnotationsTest {
val value =
WithNames.serializer().descriptor
.getElementAnnotations(1).filterIsInstance<CustomAnnotationWithDefault>().single()
assertEquals("foo", value.value)
assertEquals("default_annotation_value", value.value)
}

@Test
Expand Down Expand Up @@ -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())
}
Expand Down
9 changes: 9 additions & 0 deletions formats/json/api/kotlinx-serialization-json.api
Original file line number Diff line number Diff line change
Expand Up @@ -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 <init> (Ljava/lang/String;)V
public final synthetic fun discriminator ()Ljava/lang/String;
}

public final class kotlinx/serialization/json/JsonConfiguration {
public fun <init> ()V
public final fun getAllowSpecialFloatingPointValues ()Z
Expand Down
Loading

0 comments on commit be7af57

Please sign in to comment.