Kotlin Data Classes Cheat Sheet
Covers declaring data classes, the auto-generated equals/hashCode/toString/copy methods, destructuring, and best practices for immutable models in Kotlin.
Declaring Data Classes
A one-line declaration generates equals, hashCode, and toString.
data class User(val id: Int, val name: String, val email: String)val user1 = User(1, "Ana", "[email protected]")val user2 = User(1, "Ana", "[email protected]")println(user1 == user2) // true (structural equality via equals())println(user1) // User(id=1, name=Ana, [email protected])println(user1.hashCode() == user2.hashCode()) // true
The copy() Function
Creating a modified copy while keeping the original immutable.
data class User(val id: Int, val name: String, val email: String)val user = User(1, "Ana", "[email protected]")// copy() creates a new instance, overriding only the named parametersval renamed = user.copy(name = "Ana Maria")println(renamed) // User(id=1, name=Ana Maria, [email protected])println(user) // original is unchanged (immutability preserved with val)
Destructuring Declarations
Unpacking a data class into individual variables.
data class Point(val x: Int, val y: Int)val point = Point(10, 20)val (x, y) = point // uses auto-generated component1()/component2()println("x=$x, y=$y")// Common in loops over mapsval scores = mapOf("Ana" to 90, "Bob" to 85)for ((name, score) in scores) { println("$name scored $score")}// Underscore to skip a component you don't need (Kotlin 1.1+)val (_, onlyY) = point
Auto-Generated Members
What the compiler generates for every data class.
- equals()/hashCode()- Structural equality based on all properties declared in the primary constructor
- toString()- Readable format: ClassName(prop1=value1, prop2=value2, ...)
- copy()- Creates a shallow copy with optionally overridden properties
- componentN()- One function per constructor property, enabling destructuring
- Only primary constructor properties- Properties declared in the class body (not the constructor) are excluded from these
Data Class Rules
Constraints the compiler enforces on data class declarations.
- Primary constructor required- Must have at least one parameter, all val or var
- Cannot be abstract/open/sealed/inner- Data classes cannot be extended in these ways
- val for immutability- Prefer val properties so equals/hashCode stay consistent over the object's lifetime
- Avoid mutable var as a map/set key- Mutating it after insertion breaks hashCode-based lookups
- Can implement interfaces- Data classes can implement interfaces and have extra members beyond the constructor
Sealed Classes + Data Classes for Modeling State
Combining sealed hierarchies with data classes for exhaustive, type-safe state modeling.
sealed class UiState { data object Loading : UiState() // data object: singleton with proper toString/equals data class Success(val items: List<String>) : UiState() data class Error(val message: String, val cause: Throwable? = null) : UiState()}fun render(state: UiState) = when (state) { is UiState.Loading -> println("Loading...") is UiState.Success -> println("Got ${state.items.size} items") is UiState.Error -> println("Error: ${state.message}") // no 'else' needed -- compiler enforces exhaustiveness over a sealed hierarchy}// Success instances still get structural equality and copy() from data classval s1 = UiState.Success(listOf("a"))val s2 = s1.copy(items = listOf("a", "b"))
Overriding equals()/hashCode() Manually
Excluding specific properties from generated equality, or using a regular class when full control is needed.
// Properties declared in the class body (not the constructor) are automatically// excluded from the generated equals/hashCode/toString/copydata class CacheEntry(val key: String, val value: String) { var lastAccessed: Long = System.currentTimeMillis() // NOT part of equality}val e1 = CacheEntry("k", "v")val e2 = CacheEntry("k", "v")e2.lastAccessed = 999Lprintln(e1 == e2) // true -- lastAccessed is ignored// When you need equality that ignores a constructor property too,// drop 'data' and hand-roll equals/hashCodeclass Version(val major: Int, val minor: Int, val buildMetadata: String) { override fun equals(other: Any?): Boolean = other is Version && major == other.major && minor == other.minor override fun hashCode(): Int = 31 * major + minor override fun toString(): String = "$major.$minor"}
Data Classes with Interfaces & Default Values
Data classes implementing interfaces and using named/default arguments for flexible construction.
interface Identifiable { val id: Int}data class Product( override val id: Int, val name: String, val price: Double = 0.0, // default value val tags: List<String> = emptyList()) : Identifiable// Named arguments make call sites self-documenting and let you skip defaultsval p = Product(id = 1, name = "Widget", tags = listOf("new"))// copy() respects defaults too -- only override what changesval discounted = p.copy(price = 4.99)// Data classes CAN implement interfaces (with default members) but// cannot extend another class, since 'data' requires a fresh equals/hashCode// contract that inheritance would complicate
Serialization with @Serializable
Data classes are the natural fit for kotlinx.serialization JSON models.
import kotlinx.serialization.*import kotlinx.serialization.json.*@Serializabledata class User( val id: Int, val name: String, @SerialName("email_address") val email: String, // maps a differently-named JSON field val roles: List<String> = emptyList() // missing field falls back to default)val json = Json { ignoreUnknownKeys = true } // tolerate extra fields from the APIval user = json.decodeFromString<User>( """{"id":1,"name":"Ana","email_address":"[email protected]","extra":true}""")val text = json.encodeToString(user)// data class's structural equals() makes round-trip tests trivial:// decodeFromString<User>(encodeToString(user)) == user
Data Class vs. Alternatives
When a data class is (and isn't) the right modeling tool.
- data class- Best for immutable value objects/DTOs where structural equality and copy() matter
- value class (inline class)- Wraps a single property with zero runtime overhead; use for type-safe IDs (e.g. UserId(Int))
- data object- Singleton with data-class-style toString()/equals(), replaces 'object' for sealed hierarchy leaves
- regular class- Use when identity matters more than structural equality, or equals/hashCode need custom logic
- enum class- Use for a fixed, closed set of named constants instead of many empty data object siblings
- record-like Java interop- Data classes compile to types compatible with Java code expecting getters, but Java sees componentN() too
Never use a `var` property in a data class as a HashMap/HashSet key — mutating it after insertion changes the hashCode, and the entry becomes unfindable even though `contains()` on the original reference still works.