Skip to content

Commit

Permalink
Add @MetaSerializable annotation (#1979)
Browse files Browse the repository at this point in the history
Co-authored-by: rsinukov <rxsinukov@gmail.com>
Co-authored-by: Vsevolod Tolstopyatov <qwwdfsad@gmail.com>
  • Loading branch information
3 people authored Jul 7, 2022
1 parent be99c0d commit c232772
Show file tree
Hide file tree
Showing 4 changed files with 158 additions and 0 deletions.
3 changes: 3 additions & 0 deletions core/api/kotlinx-serialization-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ public abstract interface class kotlinx/serialization/KSerializer : kotlinx/seri
public abstract fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
}

public abstract interface annotation class kotlinx/serialization/MetaSerializable : java/lang/annotation/Annotation {
}

public final class kotlinx/serialization/MissingFieldException : kotlinx/serialization/SerializationException {
public fun <init> (Ljava/lang/String;)V
}
Expand Down
29 changes: 29 additions & 0 deletions core/commonMain/src/kotlinx/serialization/Annotations.kt
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,35 @@ public annotation class Serializable(
val with: KClass<out KSerializer<*>> = KSerializer::class // Default value indicates that auto-generated serializer is used
)

/**
* The meta-annotation for adding [Serializable] behaviour to user-defined annotations.
*
* Applying [MetaSerializable] to the annotation class `A` instructs the serialization plugin to treat annotation A
* as [Serializable]. In addition, all annotations marked with [MetaSerializable] are saved in the generated [SerialDescriptor]
* as if they are annotated with [SerialInfo].
*
* ```
* @MetaSerializable
* @Target(AnnotationTarget.CLASS)
* annotation class MySerializable(val data: String)
*
* @MySerializable("some_data")
* class MyData(val myData: AnotherData, val intProperty: Int, ...)
*
* val serializer = MyData.serializer()
* serializer.descriptor.annotations.filterIsInstance<MySerializable>().first().data // <- returns "some_data"
* ```
*
* @see Serializable
* @see SerialInfo
* @see UseSerializers
* @see Serializer
*/
@Target(AnnotationTarget.ANNOTATION_CLASS)
//@Retention(AnnotationRetention.RUNTIME) // Runtime is the default retention, also see KT-41082
@ExperimentalSerializationApi
public annotation class MetaSerializable

/**
* Instructs the serialization plugin to turn this class into serializer for specified class [forClass].
* However, it would not be used automatically. To apply it on particular class or property,
Expand Down
55 changes: 55 additions & 0 deletions core/commonTest/src/kotlinx/serialization/MetaSerializableTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package kotlinx.serialization

import kotlinx.serialization.test.*
import kotlin.reflect.KClass
import kotlin.test.*

class MetaSerializableTest {

@MetaSerializable
@Target(AnnotationTarget.CLASS, AnnotationTarget.PROPERTY)
annotation class MySerializable

@MetaSerializable
@Target(AnnotationTarget.CLASS, AnnotationTarget.PROPERTY)
annotation class MySerializableWithInfo(
val value: Int,
val kclass: KClass<*>
)

@MySerializable
class Project1(val name: String, val language: String)

@MySerializableWithInfo(123, String::class)
class Project2(val name: String, val language: String)

@MySerializableWithInfo(123, String::class)
@Serializable
class Project3(val name: String, val language: String)

@Serializable
class Wrapper(
@MySerializableWithInfo(234, Int::class) val project: Project3
)

@Test
fun testMetaSerializable() = noJsLegacy {
val serializer = serializer<Project1>()
assertNotNull(serializer)
}

@Test
fun testMetaSerializableWithInfo() = noJsLegacy {
val info = serializer<Project2>().descriptor.annotations.filterIsInstance<MySerializableWithInfo>().first()
assertEquals(123, info.value)
assertEquals(String::class, info.kclass)
}

@Test
fun testMetaSerializableOnProperty() = noJsLegacy {
val info = serializer<Wrapper>().descriptor
.getElementAnnotations(0).filterIsInstance<MySerializableWithInfo>().first()
assertEquals(234, info.value)
assertEquals(Int::class, info.kclass)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package kotlinx.serialization.features

import kotlinx.serialization.*
import kotlinx.serialization.json.*
import kotlin.test.*

class MetaSerializableJsonTest : JsonTestBase() {
@MetaSerializable
@Target(AnnotationTarget.PROPERTY, AnnotationTarget.CLASS)
annotation class JsonComment(val comment: String)

@JsonComment("class_comment")
data class IntDataCommented(val i: Int)

@Serializable
data class Carrier(
val plain: String,
@JsonComment("string_comment") val commented: StringData,
val intData: IntDataCommented
)

class CarrierSerializer : JsonTransformingSerializer<Carrier>(serializer()) {

private val desc = Carrier.serializer().descriptor
private fun List<Annotation>.comment(): String? = filterIsInstance<JsonComment>().firstOrNull()?.comment

private val commentMap = (0 until desc.elementsCount).associateBy({ desc.getElementName(it) },
{ desc.getElementAnnotations(it).comment() ?: desc.getElementDescriptor(it).annotations.comment() })

// NB: we may want to add this to public API
private fun JsonElement.editObject(action: (MutableMap<String, JsonElement>) -> Unit): JsonElement {
val mutable = this.jsonObject.toMutableMap()
action(mutable)
return JsonObject(mutable)
}

override fun transformDeserialize(element: JsonElement): JsonElement {
return element.editObject { result ->
for ((key, value) in result) {
commentMap[key]?.let {
result[key] = value.editObject {
it.remove("comment")
}
}
}
}
}

override fun transformSerialize(element: JsonElement): JsonElement {
return element.editObject { result ->
for ((key, value) in result) {
commentMap[key]?.let { comment ->
result[key] = value.editObject {
it["comment"] = JsonPrimitive(comment)
}
}
}
}
}
}

@Test
fun testMyJsonComment() {
assertJsonFormAndRestored(
CarrierSerializer(),
Carrier("plain", StringData("string1"), IntDataCommented(42)),
"""{"plain":"plain","commented":{"data":"string1","comment":"string_comment"},"intData":{"i":42,"comment":"class_comment"}}"""
)
}

}

0 comments on commit c232772

Please sign in to comment.