Skip to content

Commit c232772

Browse files
sandwwraithrsinukov1qwwdfsad
authored
Add @MetaSerializable annotation (#1979)
Co-authored-by: rsinukov <[email protected]> Co-authored-by: Vsevolod Tolstopyatov <[email protected]>
1 parent be99c0d commit c232772

File tree

4 files changed

+158
-0
lines changed

4 files changed

+158
-0
lines changed

core/api/kotlinx-serialization-core.api

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ public abstract interface class kotlinx/serialization/KSerializer : kotlinx/seri
4343
public abstract fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
4444
}
4545

46+
public abstract interface annotation class kotlinx/serialization/MetaSerializable : java/lang/annotation/Annotation {
47+
}
48+
4649
public final class kotlinx/serialization/MissingFieldException : kotlinx/serialization/SerializationException {
4750
public fun <init> (Ljava/lang/String;)V
4851
}

core/commonMain/src/kotlinx/serialization/Annotations.kt

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,35 @@ public annotation class Serializable(
7373
val with: KClass<out KSerializer<*>> = KSerializer::class // Default value indicates that auto-generated serializer is used
7474
)
7575

76+
/**
77+
* The meta-annotation for adding [Serializable] behaviour to user-defined annotations.
78+
*
79+
* Applying [MetaSerializable] to the annotation class `A` instructs the serialization plugin to treat annotation A
80+
* as [Serializable]. In addition, all annotations marked with [MetaSerializable] are saved in the generated [SerialDescriptor]
81+
* as if they are annotated with [SerialInfo].
82+
*
83+
* ```
84+
* @MetaSerializable
85+
* @Target(AnnotationTarget.CLASS)
86+
* annotation class MySerializable(val data: String)
87+
*
88+
* @MySerializable("some_data")
89+
* class MyData(val myData: AnotherData, val intProperty: Int, ...)
90+
*
91+
* val serializer = MyData.serializer()
92+
* serializer.descriptor.annotations.filterIsInstance<MySerializable>().first().data // <- returns "some_data"
93+
* ```
94+
*
95+
* @see Serializable
96+
* @see SerialInfo
97+
* @see UseSerializers
98+
* @see Serializer
99+
*/
100+
@Target(AnnotationTarget.ANNOTATION_CLASS)
101+
//@Retention(AnnotationRetention.RUNTIME) // Runtime is the default retention, also see KT-41082
102+
@ExperimentalSerializationApi
103+
public annotation class MetaSerializable
104+
76105
/**
77106
* Instructs the serialization plugin to turn this class into serializer for specified class [forClass].
78107
* However, it would not be used automatically. To apply it on particular class or property,
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package kotlinx.serialization
2+
3+
import kotlinx.serialization.test.*
4+
import kotlin.reflect.KClass
5+
import kotlin.test.*
6+
7+
class MetaSerializableTest {
8+
9+
@MetaSerializable
10+
@Target(AnnotationTarget.CLASS, AnnotationTarget.PROPERTY)
11+
annotation class MySerializable
12+
13+
@MetaSerializable
14+
@Target(AnnotationTarget.CLASS, AnnotationTarget.PROPERTY)
15+
annotation class MySerializableWithInfo(
16+
val value: Int,
17+
val kclass: KClass<*>
18+
)
19+
20+
@MySerializable
21+
class Project1(val name: String, val language: String)
22+
23+
@MySerializableWithInfo(123, String::class)
24+
class Project2(val name: String, val language: String)
25+
26+
@MySerializableWithInfo(123, String::class)
27+
@Serializable
28+
class Project3(val name: String, val language: String)
29+
30+
@Serializable
31+
class Wrapper(
32+
@MySerializableWithInfo(234, Int::class) val project: Project3
33+
)
34+
35+
@Test
36+
fun testMetaSerializable() = noJsLegacy {
37+
val serializer = serializer<Project1>()
38+
assertNotNull(serializer)
39+
}
40+
41+
@Test
42+
fun testMetaSerializableWithInfo() = noJsLegacy {
43+
val info = serializer<Project2>().descriptor.annotations.filterIsInstance<MySerializableWithInfo>().first()
44+
assertEquals(123, info.value)
45+
assertEquals(String::class, info.kclass)
46+
}
47+
48+
@Test
49+
fun testMetaSerializableOnProperty() = noJsLegacy {
50+
val info = serializer<Wrapper>().descriptor
51+
.getElementAnnotations(0).filterIsInstance<MySerializableWithInfo>().first()
52+
assertEquals(234, info.value)
53+
assertEquals(Int::class, info.kclass)
54+
}
55+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package kotlinx.serialization.features
2+
3+
import kotlinx.serialization.*
4+
import kotlinx.serialization.json.*
5+
import kotlin.test.*
6+
7+
class MetaSerializableJsonTest : JsonTestBase() {
8+
@MetaSerializable
9+
@Target(AnnotationTarget.PROPERTY, AnnotationTarget.CLASS)
10+
annotation class JsonComment(val comment: String)
11+
12+
@JsonComment("class_comment")
13+
data class IntDataCommented(val i: Int)
14+
15+
@Serializable
16+
data class Carrier(
17+
val plain: String,
18+
@JsonComment("string_comment") val commented: StringData,
19+
val intData: IntDataCommented
20+
)
21+
22+
class CarrierSerializer : JsonTransformingSerializer<Carrier>(serializer()) {
23+
24+
private val desc = Carrier.serializer().descriptor
25+
private fun List<Annotation>.comment(): String? = filterIsInstance<JsonComment>().firstOrNull()?.comment
26+
27+
private val commentMap = (0 until desc.elementsCount).associateBy({ desc.getElementName(it) },
28+
{ desc.getElementAnnotations(it).comment() ?: desc.getElementDescriptor(it).annotations.comment() })
29+
30+
// NB: we may want to add this to public API
31+
private fun JsonElement.editObject(action: (MutableMap<String, JsonElement>) -> Unit): JsonElement {
32+
val mutable = this.jsonObject.toMutableMap()
33+
action(mutable)
34+
return JsonObject(mutable)
35+
}
36+
37+
override fun transformDeserialize(element: JsonElement): JsonElement {
38+
return element.editObject { result ->
39+
for ((key, value) in result) {
40+
commentMap[key]?.let {
41+
result[key] = value.editObject {
42+
it.remove("comment")
43+
}
44+
}
45+
}
46+
}
47+
}
48+
49+
override fun transformSerialize(element: JsonElement): JsonElement {
50+
return element.editObject { result ->
51+
for ((key, value) in result) {
52+
commentMap[key]?.let { comment ->
53+
result[key] = value.editObject {
54+
it["comment"] = JsonPrimitive(comment)
55+
}
56+
}
57+
}
58+
}
59+
}
60+
}
61+
62+
@Test
63+
fun testMyJsonComment() {
64+
assertJsonFormAndRestored(
65+
CarrierSerializer(),
66+
Carrier("plain", StringData("string1"), IntDataCommented(42)),
67+
"""{"plain":"plain","commented":{"data":"string1","comment":"string_comment"},"intData":{"i":42,"comment":"class_comment"}}"""
68+
)
69+
}
70+
71+
}

0 commit comments

Comments
 (0)