Skip to content

Commit

Permalink
Introduce InheritableSerialInfo and make JsonClassDiscriminator inher…
Browse files Browse the repository at this point in the history
…itable
  • Loading branch information
sandwwraith committed Jul 20, 2021
1 parent 7bc800b commit 402be0c
Show file tree
Hide file tree
Showing 8 changed files with 101 additions and 14 deletions.
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 @@ -142,6 +142,44 @@ public annotation class Transient
@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 [ContextSerializer] on a given property or type.
* Context serializer is usually used when serializer for type can only be found in runtime.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package kotlinx.serialization

import kotlinx.serialization.descriptors.SerialDescriptor
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) {
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 @@ -56,11 +56,13 @@ class SerializersLookupEnumTest {
}

@Test
@Ignore // broken in 1.5.30
fun testEnumExternalObject() {
assertFailsWith<SerializationException> { (serializer<EnumExternalObject>()) }
}

@Test
@Ignore // broken in 1.5.30
fun testEnumExternalClass() {
assertFailsWith<SerializationException> { serializer<EnumExternalClass>() }
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ internal open class CborWriter(private val cbor: Cbor, protected val encoder: Cb

override fun encodeNull() = encoder.encodeNull()

@ExperimentalSerializationApi // KT-46731
@OptIn(ExperimentalSerializationApi::class) // KT-46731
override fun encodeEnum(
enumDescriptor: SerialDescriptor,
index: Int
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ public sealed class Json(
/**
* Creates an instance of [Json] configured from the optionally given [Json instance][from] and adjusted with [builderAction].
*/
@OptIn(ExperimentalSerializationApi::class)
public fun Json(from: Json = Json.Default, builderAction: JsonBuilder.() -> Unit): Json {
val builder = JsonBuilder(from)
builder.builderAction()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,32 +40,33 @@ 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, to configure global class discriminator, use [JsonBuilder.classDiscriminator]
* Provided key is used only for an annotated class and its subclasses;
* to configure global class discriminator, use [JsonBuilder.classDiscriminator]
* property.
*
* It is possible to define different class discriminators for different parts of class hierarchy.
* 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("class")
* @JsonClassDiscriminator("message_type")
* abstract class Base
*
* @Serializable
* @JsonClassDiscriminator("error_class")
* @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": {"class":"my.app.BaseMessage", "message": "not found"}, "error": {"error_class":"my.app.GenericError", "error_code": 404}}""")
* val message = Json.decodeFromString<Message>("""{"message": {"message_type":"my.app.BaseMessage", "message": "not found"}, "error": {"message_type":"my.app.GenericError", "error_code": 404}}""")
* ```
*
* @see JsonBuilder.classDiscriminator
*/
@SerialInfo
@InheritableSerialInfo
@Target(AnnotationTarget.CLASS)
@ExperimentalSerializationApi
public annotation class JsonClassDiscriminator(val discriminator: String)
Original file line number Diff line number Diff line change
Expand Up @@ -70,11 +70,10 @@ class JsonClassDiscriminatorTest : JsonTestBase() {
}

@Serializable
@JsonClassDiscriminator("class")
@JsonClassDiscriminator("message_type")
abstract class Base

@Serializable
@JsonClassDiscriminator("error_class")
abstract class ErrorClass : Base()

@Serializable
Expand All @@ -90,7 +89,7 @@ class JsonClassDiscriminatorTest : JsonTestBase() {


@Test
fun testDocumentationSample() {
fun testDocumentationInheritanceSample() {
val module = SerializersModule {
polymorphic(Base::class) {
subclass(BaseMessage.serializer())
Expand All @@ -103,7 +102,7 @@ class JsonClassDiscriminatorTest : JsonTestBase() {
assertJsonFormAndRestored(
Message.serializer(),
Message(BaseMessage("not found"), GenericError(404)),
"""{"message":{"class":"my.app.BaseMessage","message":"not found"},"error":{"error_class":"my.app.GenericError","error_code":404}}""",
"""{"message":{"message_type":"my.app.BaseMessage","message":"not found"},"error":{"message_type":"my.app.GenericError","error_code":404}}""",
json
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ internal open class ProtobufEncoder(
private val writer: ProtobufWriter,
@JvmField protected val descriptor: SerialDescriptor
) : ProtobufTaggedEncoder() {
@ExperimentalSerializationApi // KT-46731
@OptIn(ExperimentalSerializationApi::class) // KT-46731
public override val serializersModule
get() = proto.serializersModule

Expand Down Expand Up @@ -191,7 +191,7 @@ private class NestedRepeatedEncoder(
@JvmField val stream: ByteArrayOutput = ByteArrayOutput()
) : ProtobufEncoder(proto, ProtobufWriter(stream), descriptor) {
// all elements always have id = 1
@ExperimentalSerializationApi // KT-46731
@OptIn(ExperimentalSerializationApi::class) // KT-46731
override fun SerialDescriptor.getTag(index: Int) = ProtoDesc(1, ProtoIntegerType.DEFAULT)

override fun endEncode(descriptor: SerialDescriptor) {
Expand Down

0 comments on commit 402be0c

Please sign in to comment.