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.