Added FieldFlatteningSerializer.

This commit is contained in:
2026-03-11 22:48:20 +08:00
parent 7470bb8c34
commit e2f867a1a1
3 changed files with 234 additions and 0 deletions
@@ -0,0 +1,91 @@
package cn.rdlevel.rdkt.core.serialization
import cn.rdlevel.rdkt.core.annotations.RDKTInternalAPI
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerialInfo
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonTransformingSerializer
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.jsonObject
/**
* Annotation to indicate that a field should be flattened when serialized.
*/
@OptIn(ExperimentalSerializationApi::class)
@SerialInfo
@RDKTInternalAPI
@Target(AnnotationTarget.PROPERTY)
@Retention(AnnotationRetention.RUNTIME)
@MustBeDocumented
public annotation class Flatten
/**
* A serializer that can handle flattening of fields annotated with [Flatten].
*
* When serializing, the fields annotated with [Flatten] will be flattened into the parent JSON object.
* When deserializing, the fields annotated with [Flatten] will be deserialized from the parent JSON object,
* with elements that do not belong to any non-flattened fields.
*
* It's recommended to annotate classes that need to be flattened with [JsonIgnoreUnknownKeys][kotlinx.serialization.json.JsonIgnoreUnknownKeys]
* when there are multiple flattened fields, so that fields that only belong to some classes can be ignored when deserializing other classes.
*
* This serializer only supports JSON encoding and decoding.
*
* @param tSerializer The serializer, usually the [generated one][kotlinx.serialization.KeepGeneratedSerializer], for the type T that this serializer will delegate to.
*/
@RDKTInternalAPI
public class FieldFlatteningSerializer<T>(
tSerializer: KSerializer<T>
) : JsonTransformingSerializer<T>(tSerializer) {
private val flattenFieldNames: MutableList<String> = mutableListOf()
private val notFlattenFieldNames: MutableList<String> = mutableListOf()
init {
(0..<descriptor.elementsCount).forEach {
val name = descriptor.getElementName(it)
if (descriptor.getElementAnnotations(it).filterIsInstance<Flatten>().isEmpty()) {
notFlattenFieldNames += name
} else {
flattenFieldNames += name
}
}
}
override fun transformSerialize(element: JsonElement): JsonElement {
return buildJsonObject {
element.jsonObject.forEach { (k, v) ->
if (k !in flattenFieldNames) {
put(k, v)
} else {
v.jsonObject.forEach { (k2, v2) ->
put(k2, v2)
}
}
}
}
}
override fun transformDeserialize(element: JsonElement): JsonElement {
return buildJsonObject base@{
val flattened = buildJsonObject flattened@{
element.jsonObject.forEach { (k, v) ->
if (k in notFlattenFieldNames) {
this@base.put(k, v)
} else {
put(k, v)
}
}
}
flattenFieldNames.forEach {
put(it, flattened)
}
}
}
}
/**
* Converts a [KSerializer] into a [FieldFlatteningSerializer] that can handle flattening of fields annotated with [Flatten].
*/
@RDKTInternalAPI
public fun <T> KSerializer<T>.flatten(): FieldFlatteningSerializer<T> = FieldFlatteningSerializer(this)
@@ -0,0 +1,101 @@
package cn.rdlevel.rdkt.core.serialization
import cn.rdlevel.rdkt.core.annotations.RDKTInternalAPI
import kotlinx.serialization.KSerializer
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.*
import kotlinx.serialization.serializer
import kotlin.reflect.KMutableProperty1
/**
* A [KSerializer] that flattens the fields of the serialized type into the same level as the base type.
*
* Fields to be flattened should be annotated with [kotlinx.serialization.Transient],
* and types to be flattened or serialized should be annotated with [JsonIgnoreUnknownKeys].
*
* This serializer only supports JSON encoding and decoding.
*
* This serializer is legacy, but I want to keep it because I think it's cool.
*
* @property tSerializer The original serializer for the base type.
*/
@RDKTInternalAPI
public class LegacyFieldFlatteningSerializer<Type>(
private val tSerializer: KSerializer<Type>,
) : KSerializer<Type> {
public class Entry<T, V>(
private val serializer: KSerializer<V>,
private val supplier: (T) -> V,
private val consumer: (T, V) -> Unit,
) {
public fun encodeEntry(builder: JsonObjectBuilder, json: Json, baseValue: T) {
json.encodeToJsonElement(serializer, supplier(baseValue)).jsonObject.forEach { (k, v) ->
builder.put(k, v)
}
}
public fun decodeEntry(json: Json, baseValue: T, element: JsonElement) {
consumer(baseValue, json.decodeFromJsonElement(serializer, element))
}
}
private val flatteningEntries: MutableList<Entry<Type, *>> = mutableListOf()
public fun flatten(entry: Entry<Type, *>) {
flatteningEntries.add(entry)
}
override val descriptor: SerialDescriptor get() = tSerializer.descriptor
override fun serialize(encoder: Encoder, value: Type) {
val jsonEncoder = encoder as? JsonEncoder
?: error("${LegacyFieldFlatteningSerializer::class.simpleName} only supports JSON encoding.")
val element = buildJsonObject {
val json = jsonEncoder.json
json.encodeToJsonElement(tSerializer, value).jsonObject.forEach { (k, v) ->
put(k, v)
}
flatteningEntries.forEach { it.encodeEntry(this, json, value) }
}
jsonEncoder.encodeJsonElement(element)
}
override fun deserialize(decoder: Decoder): Type {
val jsonDecoder = decoder as? JsonDecoder
?: error("${LegacyFieldFlatteningSerializer::class.simpleName} only supports JSON decoding.")
val element = jsonDecoder.decodeJsonElement()
val json = jsonDecoder.json
val value = json.decodeFromJsonElement(tSerializer, element)
flatteningEntries.forEach { it.decodeEntry(json, value, element) }
return value
}
}
/**
* Adds a flattened field to the serializer.
*/
@RDKTInternalAPI
public inline fun <Ser : LegacyFieldFlatteningSerializer<T>, T, reified F> Ser.flatten(
property: KMutableProperty1<T, F>,
fieldSerializer: KSerializer<F>? = null,
): Ser = apply {
flatten(
LegacyFieldFlatteningSerializer.Entry(
fieldSerializer ?: serializer<F>(),
property::get,
property::set,
)
)
}
/**
* Converts a [KSerializer] to a [LegacyFieldFlatteningSerializer] and adds a flattened field to it.
*/
@RDKTInternalAPI
public inline fun <T, reified F> KSerializer<T>.flatten(
property: KMutableProperty1<T, F>,
fieldSerializer: KSerializer<F>? = null,
): LegacyFieldFlatteningSerializer<T> = LegacyFieldFlatteningSerializer(this).flatten(property, fieldSerializer)
@@ -0,0 +1,42 @@
@file:OptIn(RDKTInternalAPI::class, ExperimentalSerializationApi::class)
package cn.rdlevel.rdkt.core.test
import cn.rdlevel.rdkt.core.annotations.RDKTInternalAPI
import cn.rdlevel.rdkt.core.serialization.Flatten
import cn.rdlevel.rdkt.core.serialization.flatten
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.KSerializer
import kotlinx.serialization.KeepGeneratedSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import kotlin.test.Test
import kotlin.test.assertEquals
class FieldFlatteningSerializerTest {
@Serializable
data class Child(
val a: Int,
val b: String,
)
@Serializable(Parent.Serializer::class)
@KeepGeneratedSerializer
data class Parent(
val c: Double,
val d: Boolean,
@Flatten
val child: Child,
) {
object Serializer : KSerializer<Parent> by generatedSerializer().flatten()
}
@Test
fun test() {
val data = Parent(1.23, true, Child(42, "hello"))
val json = Json.encodeToString(Parent.Serializer, data)
assertEquals("""{"c":1.23,"d":true,"a":42,"b":"hello"}""", json)
val data2: Parent = Json.decodeFromString(json)
assertEquals(data2, data2)
}
}