diff --git a/core/api/kotlinx-serialization-core.api b/core/api/kotlinx-serialization-core.api index 53d270941..63c7dfa60 100644 --- a/core/api/kotlinx-serialization-core.api +++ b/core/api/kotlinx-serialization-core.api @@ -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 (Ljava/lang/String;)V } diff --git a/core/commonMain/src/kotlinx/serialization/Annotations.kt b/core/commonMain/src/kotlinx/serialization/Annotations.kt index 45b5c1c06..d8864d184 100644 --- a/core/commonMain/src/kotlinx/serialization/Annotations.kt +++ b/core/commonMain/src/kotlinx/serialization/Annotations.kt @@ -73,6 +73,35 @@ public annotation class Serializable( val with: KClass> = 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().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, diff --git a/core/commonTest/src/kotlinx/serialization/MetaSerializableTest.kt b/core/commonTest/src/kotlinx/serialization/MetaSerializableTest.kt new file mode 100644 index 000000000..04eded26d --- /dev/null +++ b/core/commonTest/src/kotlinx/serialization/MetaSerializableTest.kt @@ -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() + assertNotNull(serializer) + } + + @Test + fun testMetaSerializableWithInfo() = noJsLegacy { + val info = serializer().descriptor.annotations.filterIsInstance().first() + assertEquals(123, info.value) + assertEquals(String::class, info.kclass) + } + + @Test + fun testMetaSerializableOnProperty() = noJsLegacy { + val info = serializer().descriptor + .getElementAnnotations(0).filterIsInstance().first() + assertEquals(234, info.value) + assertEquals(Int::class, info.kclass) + } +} diff --git a/formats/json-tests/commonTest/src/kotlinx/serialization/features/MetaSerializableJsonTest.kt b/formats/json-tests/commonTest/src/kotlinx/serialization/features/MetaSerializableJsonTest.kt new file mode 100644 index 000000000..25efa6b40 --- /dev/null +++ b/formats/json-tests/commonTest/src/kotlinx/serialization/features/MetaSerializableJsonTest.kt @@ -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(serializer()) { + + private val desc = Carrier.serializer().descriptor + private fun List.comment(): String? = filterIsInstance().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) -> 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"}}""" + ) + } + +}