Kopykat
Little utilities for more pleasant immutable data in Kotlin
Install / Use
/learn @kopykat-kt/KopykatREADME
This project is currently on maintenance mode. We recommend to try Kopy if you need a similar feature for Kotlin 2.x.
One of the great features of Kotlin data classes is
their copy method. But using it can become cumbersome very
quickly, because you need to repeat the name of the field before and after.
data class Person(val name: String, val age: Int)
val p1 = Person("Alex", 1)
val p2 = p1.copy(age = p1.age + 1) // too many 'age'!
This plug-in generates a few new methods that make working with immutable (read-only) types, like data classes and value classes, more convenient.

Those methods can be divided in two big groups:
- Same-type transformations, which take a value of a certain type and produce a copy
of the same type by changing a few fields. Those methods are closer to the
copymethod available in Kotlin. - Isomorphic copy constructors, which produce a value of a different type from the given one, provided that they both contain the same fields. Those methods are very useful when interfacing across different layers of the application.
Same-type transformations
KopyKat extends Kotlin's built-in copy with a version based on mutable copies, and a version based on maps (that is,
you state the changes to be done to the values based on the old ones instead of the new values themselves.) In addition,
the default copy is extended to work on sealed hierarchies and value classes.
Mutable copy
This new version of copy takes a block as a parameter. Within that block, mutability is simulated; the final
assignment of each (mutable) variable becomes the value of the new copy. These are generated for both data classes and
value classes.
val p1 = Person("Alex", 1)
val p2 = p1.copy {
age++
}
You can use old to access the previous (immutable) value, before any changes.
val p3 = p1.copy {
age++
if (notTheirBirthday) {
age = old.age // get the previous value
}
}
Nested mutation
If you have a data class that contains another data class (or value class) as a property, you can also make changes to inner types. Let's say we have these types:
data class Person(val name: String, val job: Job)
data class Job(val title: String, val teams: List<String>)
val p1 = Person(name = "John", job = Job("Developer", listOf("Kotlin", "Training")))
Currently, to do mutate inner types you have to do the following:
val p2 = p1.copy(job = p1.job.copy(title = "Señor Developer"))
With KopyKat you can do this in a more readable way:
val p2 = p1.copy { job.title = "Señor Developer" }
Warning For now, this doesn't work with types that are external to the source code (i.e. dependencies). We are working on supporting this in the future.
Nested collections
The nested mutation also extends to collections, which are turned into their mutable counterparts, if they exist.
val p3 = p1.copy { job.teams.add("Compiler") }
To avoid unnecessary copies, we recommend to mutate the collections in-place as much as possible. This means that
forEach functions and mutation should be preferred over map.
val p4 = p1.copy { // needs an additional toMutableList at the end
job.teams = job.teams.map { it.capitalize() }.toMutableList()
}
val p5 = p1.copy { // mutates the job.teams collection in-place
job.teams.forEachIndexed { i, team -> job.teams[i] = team.capitalize() }
}
The at.kopyk:mutable-utils library (documentation) contains versions of the main collection functions which reuse the same structure.
val p6 = p1.copy { // mutates the job.teams collection in-place
job.teams.mutateAll { it.capitalize() }
}
<hr>
Mapping copyMap
Instead of new values, copyMap takes as arguments the transformations that ought to be applied to each argument.
The "old" value of each field is given as argument to each of the functions, so you can refer to it using it or
introduce an explicit name.
val p1 = Person("Alex", 1)
val p2 = p1.copyMap(age = { it + 1 })
val p3 = p1.copyMap(name = { nm -> nm.capitalize() })
The whole "old" value (the Person in the example above) is given as receiver to each of the transformations. That
means that you can access all the other fields in the body of each of the transformations.
val p4 = p1.copyMap(age = { name.count() })
When using value classes, given that you only have one property, you can skip the name of the property.
@JvmInline value class Age(ageValue: Int)
val a = Age(39)
val b = a.copyMap { it + 1 }
You can use copyMap to simulate copy, by making the transformation return a constant value.
val p5 = p1.copyMap(age = { 10 })
<hr>
copy for sealed hierarchies
KopyKat also works with sealed hierarchies. These are both sealed classes and sealed interfaces. It generates
regular copy, copyMap, and mutable copy for the common properties, which ought to be declared in the parent class.
abstract sealed class User(open val name: String)
data class Person(override val name: String, val age: Int): User(name)
data class Company(override val name: String, val address: String): User(name)
This means that the following code works directly, without requiring an intermediate when.
fun User.takeOver() = this.copy { name = "Me" }
Equally, you can use copyMap in a similar fashion:
fun User.takeOver() = this.copyMap(name = { "Me" })
Or, you can use a more familiar copy function:
fun User.takeOver() = this.copy(name = "Me")
<hr>Warning KopyKat only generates these if all the subclasses are data or value classes. We can't mutate object types without breaking the world underneath them. And cause a lot of pain.
copy for type aliases
KopyKat can also generate the different copy methods for a type alias.
@CopyExtensions
typealias Person = Pair<String, Int>
// generates the following methods
fun Person.copyMap(first: (String) -> String, second: (Int) -> Int): Person = TODO()
fun Person.copy(block: `Person$Mutable`.() -> Unit): Person = TODO()
The following must hold for the type alias to be processed:
- It must be marked with the
@CopyExtensionsannotation, - It must refer to a data or value class, or a type hierarchy of those.
Isomorphic copy constructors
We know, isomorphic seems like a big word. However, it just means that two things are similar in some way. In this case KopyKat can generate copy constructors between two types that have the same properties, with the same name, and the same type.
In Kotlin, a copy constructor is a top level function with the same name as the type (in PascalCase)
that returns the given type. This naming pattern is described in the official Kotlin Code
Conventions.
To generate these you have to annotate your types with one of the following:
@Copy@CopyFrom@CopyTo
All of them take another type to copy from/to, as a parameter. In the case of @Copy, it will
generate two functions for both directions. So, if we have code like this:
data class Person(val name: String, val age: Int)
@Copy(Person::class)
data class LocalPerson(val name: String, val age: Int)
The following code is generated:
inline fun Person(from: LocalPerson): Person =
Person(name = from.name, age = from.age)
inline fun LocalPerson(from: Person): Person =
LocalPerson(name = from.name, age = from.age)
These allow to convert from one type to the other and vice versa. This is quite a common pattern used to cross boundaries of the different layers of an application. Often, they are called mappers.
If you don't need either of the copy constructors, you can use either @CopyFrom or @CopyTo. @CopyFrom will
generate a copy constructor from the provided type to the annotated type (LocalPerson -> Person). On the other hand,
if you use @CopyTo will generate the oposite (Person -> LocalPerson).
Nested copy constructors
In some cases you may want to have properties that are different on both types. To support data trees like that, you must make sure that they have copy constructors generated as well. For example:
data class Person(val name: String, val job: Job)
data class Job(val title: String)
@Copy(Person::class) data class LocalPerson(val name: String, val job: LocalJob)
@Copy(Job::class) data class LocalJob(val title: String)
val localPerson = LocalPerson("Alice", LocalJob("Developer"))
val person = Person(localPerson)
check(person.name == "Alice")
check(person.job.title == "Developer")
In this example, if LocalJob is not annotated wih @Copy (or @CopyFrom) the compiler will complain about it.
Multiple copy constructors
Annotations for copy constructors (@Copy[From|To]) can be applied more than once to the same type. This means
that you can define mapping across multiple isomorphic types:
@Copy(LocalPerson::class)
@Copy(RemotePerson::class)
data class Person(val name: String, val age: Int)
data class LocalPerson(val name: String, val age: Int)
data class RemotePerson(val name: String, val age: Int)
This configuration will generate 4 different copy constructors.
<hr style="border-bottom: 3px dashed #b5e853;">Using KopyKat in your project
This
Related Skills
node-connect
347.2kDiagnose OpenClaw node connection and pairing failures for Android, iOS, and macOS companion apps
frontend-design
108.0kCreate distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
openai-whisper-api
347.2kTranscribe audio via OpenAI Audio Transcriptions API (Whisper).
qqbot-media
347.2kQQBot 富媒体收发能力。使用 <qqmedia> 标签,系统根据文件扩展名自动识别类型(图片/语音/视频/文件)。
