Kotlin read-only val, data class, and
kotlinx-serialization
provide us a nice way to structure our data in concurrent programs.
However, we experienced the following inconveniences:
- Need to manually register subclasses for open-polymorphism.
- Sometimes we need both a mutable version and an immutable version of the same data.
This library is an attempt to improve the development experience.
Add Maven Central to the repositories:
repositories {
mavenCentral()
}Add the Google ksp plugin:
plugins {
id("com.google.devtools.ksp") version "$kspVersion"
}Add the ksergen-ksp dependency:
dependencies {
ksp("com.github.adriankhl.ksergen:ksergen-ksp:$ksergenVersion")
}You may also want to disable code generation for test such that tests are forced to refer to
the generated codes in main:
afterEvaluate {
tasks.named("kspTestKotlin") {
enabled = false
}
}Whenever you build your source code (e.g., gradle build),
the ksergen-ksp will scan the parents of your @Serializable classes
to generate a GeneratedModule object at the ksergen package:
public object GeneratedModule {
public val serializersModule: SerializersModule = SerializersModule {
polymorphic(SerializableParentData::class) {
subclass(SimpleSerializableData::class)
}
polymorphic(MutableSerializableParentData::class) {
subclass(MutableExternalMasterData::class)
}
polymorphic(SerializableParentData::class) {
subclass(ExternalMasterData::class)
}
}
}You can then use the generated serializersModule for your serializers:
val format = Json {
encodeDefaults = true
serializersModule = GeneratedModule.serializersModule
}
val a = MutableExternalPolymorphicData()
val b: String = format.encodeToString(a)You can add following block in build.gradle.kts to change the package of GeneratedModule:
ksp {
arg("generatedModulePackage", "my.package")
}In addition to the ksp dependency,
you need to add the ksp-annotations dependency:
dependencies {
ksp("com.github.adriankhl.ksergen:ksergen-annotations:$ksergenVersion")
}The name of a mutable data class has to start with Mutable,
you can apply the @GenerateImmutable to automatically generate an
immutable version of the class within the same package.
Original code:
@GenerateImmutable
data class MutableIntData(var i1: Int = 1, var i2: Int = 2)
@GenerateImmutable
@SerialName("Demo")
data class MutableDemoData(
var id: MutableIntData = MutableIntData(),
var il: MutableList<Int> = mutableListOf(1, 2),
var idl: MutableList<MutableIntData> = mutableListOf(MutableIntData()),
)Generated code:
@Serializable
@SerialName("ksergen.mock.base.MutableIntData")
public data class IntData(
public val i1: Int,
public val i2: Int,
)
@Serializable
@SerialName(`value` = "Demo")
public data class DemoData(
public val id: IntData,
public val il: List<Int>,
public val idl: List<IntData>,
)The @GenerateImmutable annotation itself is annotated with
MetaSerializable,
so the annotated data class is serializable.
The generated immutable data class has a serialName that is the same
with the serialName of the original mutable class.
The GeneratedModule also takes into account of these classes.
This is a sample test code to show how the serialization works:
fun serializationTest() {
val format = Json {
encodeDefaults = true
serializersModule = GeneratedModule.serializersModule
}
val a = MutableDemoData()
val b: String = format.encodeToString(a)
val c: DemoData = format.decodeFromString(b)
val d: String = format.encodeToString(c)
val e: MutableDemoData = format.decodeFromString(d)
assertEquals(a, e)
}In our use case, it would be handy if we can also copy default values and member functions to the generated class. Unfortunately, because ksp does not support expression-level information, this is not possible.
Instead, you can use serialization to default-initialize immutable data classes and use extension functions to emulate member functions of immutable data classes:
fun IntData.sum(): Int = i1 + i2
fun MutableIntData.sum(): Int = i1 + i2
fun sumTest() {
val format = Json {
encodeDefaults = true
serializersModule = GeneratedModule.serializersModule
}
val mid = MutableIntData()
val id: IntData = format.decodeFromString(format.encodeToString(mid))
val s1 = mid.sum()
val s2 = id.sum()
assertEquals(s1, s2)
}kopykat implements a copy method to modify a
nynested immutable data class. Actually, this library is inspired by kopykat,
but we created this library since the solution
provided by kopykat doesn't suit our need.