KReplica is a DTO generator for KMP and Kotlin JVM. It runs automatically during Kotlin compilation.
Also check out the new website for KReplica (its documentation is more up-to-date than the README).
- Variant generation: From a single interface, specify up to three variants to generate:
DATA: For read-only data representationCREATE: For object creation requestsPATCH: For update requests, wraps properties in aPatchabletype.
- Granular control: Enable or disable specific features or variants at both the model and property levels.
- Nominal Typing: Automatically wrap primitive types in value classes for added type safety and clarity.
- Schema Versioning: Defines a sealed interface for each schema version, enabling exhaustive when expressions.
- Plain Kotlin: Emits plain Kotlin source files to your build directory, free of framework-specific code or runtime dependencies.
Add the KSP and KReplica plugins to your module's build.gradle.kts:
plugins {
id("com.google.devtools.ksp") version "2.2.10-2.0.2" // Use a KSP version that matches your Kotlin version
id("io.availe.kreplica") version "5.0.1"
}KReplica generates sealed interfaces, allowing you to leverage exhaustive when expressions for robust and type-safe code. This makes handling different versions or variants straightforward and ensures you don't miss cases.
In example 2 (versioned schema), if you check the generated code, here is a short snippet of what you will see:
public sealed interface V2 : UserAccountSchema {
public data class Data(
public val id: Int,
public val name: String,
public val schemaVersion: Int = 2,
) : V2,
DataVariantThis code snippet might seem a bit complex for a single DTO, but it's key to the examples below:
Example: Exhaustively handle all schema versions and variants
fun handleUser(user: UserAccountSchema) {
when (user) {
is UserAccountSchema.V1.Data -> TODO()
is UserAccountSchema.V2.Data -> TODO()
is UserAccountSchema.V2.PatchRequest -> TODO()
}
}Example: Exhaustively handle only a specific schema version
fun handleUser(user: UserAccountSchema.V2) {
when (user) {
is UserAccountSchema.V2.Data -> TODO()
is UserAccountSchema.V2.PatchRequest -> TODO()
}
}Example: Exhaustively handle only a specific kind of variant (e.g., all data variants)
fun handleUser(user: UserAccountSchema.DataVariant) {
when (user) {
is UserAccountSchema.V1.Data -> TODO()
is UserAccountSchema.V2.Data -> TODO()
}
}Example: Target only a specific schema version and specific variant
fun handleUser(user: UserAccountSchema.V1.Data) {
print(user.id)
}Note 1: KReplica generates all output files in your module’s build/generated-src/kotlin-poet/ directory.
Note 2: 90% of what you need to know to utilize KReplica is covered by the first two examples:
Replicate.ModelandReplicate.Property(example 1)- Versioned schemas (example 2)
This example covers how to use the Replicate.Model and Replicate.Property annotations.
@OptIn(ExperimentalUuidApi::class)
@Replicate.Model(
variants = [DtoVariant.DATA, DtoVariant.CREATE, DtoVariant.PATCH], // required argument
nominalTyping = NominalTyping.ENABLED // disabled by default
)
private interface UserAccount {
// This property inherits all of @Replicate.Model's arguments
val emailAddress: String
// This property is only included in the DATA variant
@Replicate.Property(include = [DtoVariant.DATA])
val id: Uuid
// This property is excluded from the CREATE variant
@Replicate.Property(exclude = [DtoVariant.CREATE])
val banReason: Patchable<List<String?>>
// We opt out of nominalTyping for this property
@Replicate.Property(nominalTyping = NominalTyping.DISABLED)
val userDescription: String?
}See the generated code.
See the patchable file
To version a Replicate.Model declaration, create a base interface (e.g. UserAccount) and extend it with V<number>
interfaces (e.g. V1, V2) to track model changes.
private interface UserAccount
@Replicate.Model(variants = [DtoVariant.DATA])
private interface V1 : UserAccount {
val id: Int
}
@Replicate.Model(variants = [DtoVariant.DATA, DtoVariant.PATCH])
private interface V2 : UserAccount {
val id: Int
val name: String
}See the generated code
If you wish to not follow the V<number> naming convention, you must use the Replicate.SchemaVersion annotation to
manually
specify a version number.
private interface UserAccount
@Replicate.Model(variants = [DtoVariant.DATA, DtoVariant.PATCH])
@Replicate.SchemaVersion(1)
private interface NewAccount : UserAccount {
val id: Int
val name: String
}See the generated code
Note all versioned declarations automatically inject a schema_version property into the generated DTOs.
If the annotation can be applied on interfaces, you can directly use it without the need for Replicate.Apply. For
example:
@Replicate.Model(variants = [DtoVariant.DATA])
@Deprecated("Use NewUserAccount instead")
private interface UserAccount {
@Deprecated("Use newId instead")
val id: Int
}See the generated code
Interfaces cannot directly implement some annotations, including Serializable. Instead, you can use Note
Replicate.Apply.
@OptIn(ExperimentalTime::class)
object InstantAsStringSerializer : KSerializer<Instant> {
override val descriptor: SerialDescriptor =
PrimitiveSerialDescriptor("Instant", PrimitiveKind.STRING)
override fun serialize(encoder: Encoder, value: Instant) {
encoder.encodeString(value.toString())
}
override fun deserialize(decoder: Decoder): Instant {
return Instant.parse(decoder.decodeString())
}
}private interface UserAccount // this is a versioned schema declaration
@OptIn(ExperimentalTime::class)
@Replicate.Model(variants = [DtoVariant.DATA], autoContextual = AutoContextual.DISABLED)
@Replicate.Apply([Serializable::class])
private interface V1 : UserAccount {
val id: Int
// here we manually apply contextual, as it inherits the model's AutoContextual.DISABLED
@Contextual
val startTime: Instant
// here we manually specify which serializer we want to use
@Serializable(with = InstantAsStringSerializer::class)
val midTime: Instant
// here we override autoContextual to be ENABLED and let KReplica apply it automatically
@Replicate.Property(autoContextual = AutoContextual.ENABLED)
val endTime: List<List<Instant>>
}See the generated code
Note that Replicate.Apply can also take include/exclude arguments, if you want an annotation to only be
applied to a
specific variant. Additionally, Replicate.Apply is repeatable and takes a list of annotations.
The drawback of Replicate.Apply is that the IDE no longer warns when Contextual is needed. To address
this,
KReplica recursively applies
Contextual in generated code, so it works regardless of generic nesting.
If you wish, you can turn off the autoContextual flag on Replicate.Model or Replicate.Property.
Types exempt from Contextual are whitelisted in codegen-models/src/commonMain/kotlin/io/availe/models/Constants.kt.
The @Replicate.Hide annotation stops a Replicate.Model declaration from being generated. It's mainly for temporarily
testing how
code removal affects the system—but use it as you see fit.
@Replicate.Model(variants = [DtoVariant.DATA])
@Replicate.Hide
private interface UserAccount {
val id: Int
}Say that you previously defined UserAccount:
private interface UserAccount
@Replicate.Model(variants = [DtoVariant.DATA, DtoVariant.CREATE, DtoVariant.PATCH], nominalTyping = NominalTyping.ENABLED)
private interface V1 : UserAccount {
val id: Int
}Now you want UserAccount to be included in AdminAccount, but in a particular format:
AdminAccountDATA variant should includeUserAccountdata variantAdminAccountcreate variant should includeUserAccountcreate variantAdminAccountpatch variant should includeUserAccountpatch variant
You could use the Replicate.Property annotation to manually configure each field, or you can take advantage of
contextual nested models:
@Replicate.Model(variants = [DtoVariant.DATA, DtoVariant.CREATE, DtoVariant.PATCH])
private interface AdminAccount {
val user: UserAccountSchema.V1
}See the generated code
Or if UserAccount was a non-versioned schema:
@Replicate.Model(variants = [DtoVariant.DATA, DtoVariant.CREATE, DtoVariant.PATCH])
private interface AdminAccount {
val user: UserAccountSchema
}See the generated code
No. The replication of all children must be a subset of the parent (⊆), including for nested models. Otherwise, KReplica will error and log the offending fields. This rule ensures fail-fast feedback. If you restrict a parent’s replication but forget to update a child field, you’ll get an immediate build-time error.
No. KReplica actually cleans the build folder at the start of each run (which ensures no stale data). To ensure that
nested contextuals work, KReplica uses two-pass compilation. Prior to the main compilation, stub files of all
Replicate.Model declarations, which is then deleted to avoid a redeclaration error.
The private keyword is not required for KReplica to function. However, the KReplica interfaces are useless outside
of KReplica, so I prefer to private them so they don't contaminate the name space.
This is particularly important with versioned schemas, as the V[number] naming convention is repetitive and you cannot redeclare interfaces. This does mean, however, that only one versioned schema can exist per file.