data class
A Kotlin data class auto-generates equals, hashCode, toString, componentN, and copy. Perfect for DTOs, API responses, value objects.
Generated methods, copy, destructuring
EXAMPLE
// 1) Declare
data class User(
val id: Long,
val name: String,
val email: String,
val createdAt: Instant = Instant.now(),
)
val u = User(1, "Ada", "a@x.com")
println(u) // User(id=1, name=Ada, email=a@x.com, createdAt=...)
// 2) Value equality (NOT identity)
val u1 = User(1, "Ada", "a@x.com")
val u2 = User(1, "Ada", "a@x.com")
println(u1 == u2) // true (value equality)
println(u1 === u2) // false (reference equality)
println(u1.hashCode() == u2.hashCode()) // true
// 3) copy — non-destructive mutation
val older = u.copy(name = "Ada L.")
val renamed = u.copy(email = "ada@example.com")
// u is unchanged.
// 4) componentN + destructuring
val (id, name, email) = u // destructures via component1(), component2(), ...
println("\${id} \${name}")
// In a loop
val users = listOf(User(1, "Ada", "a@x"), User(2, "Bo", "b@x"))
for ((id, name, _) in users) {
println("\${id}: \${name}")
}
// 5) Data class restrictions
// - Must have at least ONE primary constructor parameter
// - All primary ctor params must be val or var
// - Can't be abstract, open, sealed, or inner
// - You CAN add methods + companions + body code beyond the ctor
data class Money(val amount: Double, val currency: String) {
val display: String get() = "%.2f %s".format(amount, currency)
fun convert(rate: Double, currency: String) = Money(amount * rate, currency)
companion object {
val ZERO_AUD = Money(0.0, "AUD")
}
}
val m = Money(9.99, "AUD")
println(m.display) // 9.99 AUD
// 6) Override componentN / equals / hashCode if needed
data class CaseInsensitiveTag(val value: String) {
override fun equals(other: Any?): Boolean {
if (other !is CaseInsensitiveTag) return false
return value.lowercase() == other.value.lowercase()
}
override fun hashCode(): Int = value.lowercase().hashCode()
}
// 7) Data classes + JSON (kotlinx.serialization)
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
@@Serializable
data class Post(
val id: Long,
val title: String,
val body: String,
val tags: List<String> = emptyList(),
)
val p = Post(1, "Hi", "first", listOf("intro"))
val json = Json.encodeToString(Post.serializer(), p)
val back = Json.decodeFromString(Post.serializer(), json)
// 8) Used as map keys
val prices = mutableMapOf<Money, Int>()
prices[Money(9.99, "AUD")] = 100
prices[Money(9.99, "AUD")] // 100 — value equality
// 9) Generic data class
data class Result<T>(val value: T, val error: String? = null) {
val isOk: Boolean get() = error == null
}
fun <T> success(value: T) = Result(value)
fun <T> failure(message: String) = Result<T>(value = null as T, error = message)
// 10) Sealed class with data class variants (discriminated union)
sealed class ApiResult<out T> {
data class Success<T>(val data: T) : ApiResult<T>()
data class Failure(val code: Int, val message: String) : ApiResult<Nothing>()
object Loading : ApiResult<Nothing>()
}
fun <T> handle(r: ApiResult<T>) = when (r) {
is ApiResult.Success -> println(r.data)
is ApiResult.Failure -> println("err \${r.code}: \${r.message}")
ApiResult.Loading -> println("loading")
}
// 11) Compare with regular class
class User2(
val id: Long,
val name: String,
)
val r1 = User2(1, "Ada")
val r2 = User2(1, "Ada")
println(r1 == r2) // false — default equals is reference equality
// 12) Nested data classes
data class Order(
val id: String,
val customer: Customer,
val items: List<OrderItem>,
)
data class Customer(val id: Long, val name: String, val email: String)
data class OrderItem(val sku: String, val qty: Int, val price: Double)
val o = Order("o_1", Customer(1, "Ada", "a@x"), listOf(OrderItem("A100", 2, 9.99)))
// Deep copy via copy()
val discounted = o.copy(
items = o.items.map { it.copy(price = it.price * 0.9) }
)
// 13) Default values for ctor params
data class Settings(
val theme: String = "auto",
val notifications: Boolean = true,
val pageSize: Int = 25,
)
val s = Settings() // all defaults
val s2 = Settings(theme = "dark") // override one
val s3 = Settings(pageSize = 50, notifications = false)
// 14) Convert between data classes
data class DomainUser(val id: Long, val name: String)
data class ApiUser(val id: Long, val name: String, val email: String)
fun ApiUser.toDomain() = DomainUser(id, name)
fun DomainUser.toApi(email: String) = ApiUser(id, name, email)
// 15) Common patterns
// API DTOs
@@Serializable data class CreatePostRequest(val title: String, val body: String, val tags: List<String>)
@@Serializable data class CreatePostResponse(val id: Long, val createdAt: Long)
// Form state in Compose
data class LoginUiState(
val email: String = "",
val password: String = "",
val busy: Boolean = false,
val error: String? = null,
) {
val isValid: Boolean get() = email.contains('@@') && password.length >= 8
}
// Domain events
sealed class CartEvent {
data class ItemAdded(val sku: String, val qty: Int) : CartEvent()
data class ItemRemoved(val sku: String) : CartEvent()
object Cleared : CartEvent()
}
// 16) Tips + bugs
// • Don't put MUTABLE fields (var) in data classes if you'll use as map keys
// • Equals depends on PRIMARY ctor params only — not body fields
// • toString includes ALL primary ctor params — careful with secrets
// • copy() is shallow — nested mutable types must be copied separately
// • Use sealed + data for typed discriminated unions
Why it matters
Data classes replace 80% of POCOs. Free equals / hashCode / copy / destructuring — use them for DTOs, API responses, Compose UI state. Combine with sealed classes for type-safe discriminated unions.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
data class Point(val x: Int, val y: Int) val p = Point(1, 2) val p2 = p.copy(y = 5) // free equals/hashCode/toString/copyTry it Yourself »
Exercise
Generate equals/hashCode/copy automatically.
class Point(val x: Int, val y: Int)
Four letters.
Discussion
Loading…