# Data classes

Source: https://codewiki.com/kotlin/data-classes/

> - **what**: A data class defines a set of values through its primary-constructor properties, and the compiler generates equality, hashing, string, destructuring, and copying members.
> - **trap**: `copy()` is shallow and body properties are excluded from generated members; `val` also doesn't make nested objects immutable.
> - **fix**: Put all state that belongs to the value semantics in the primary constructor, and keep the whole object graph used by equality and hashing stable.

## What it is and why it exists

A data class is a class marked with `data`, intended for objects described mainly by a set of values. From its primary-constructor properties, the compiler generates the usual `equals()`, `hashCode()`, `toString()`, `componentN()`, and `copy()` members, avoiding boilerplate whose parts can easily drift out of sync.

Data classes solve more than a code-length problem. They place the object's value semantics at the entry to its declaration: properties in the primary constructor participate by default in comparison, hashing, display, destructuring, and copying. When you review a model, this boundary tells you when two instances represent the same value.

You encounter data classes in API data-transfer objects, immutable state snapshots, configuration values, event payloads, and small domain values. A data class isn't automatically a database entity or immutable. It fits when the object should compare by content and its state can be expressed stably as constructor properties.

An ordinary class inherits identity-based behavior from `Any.equals()` by default. A data class instead supplies structural equality: `a == b` calls `equals()`, and two distinct instances can produce `true` when their participating properties are equal. Use `===` when you need to ask whether two references point to the same instance.

One practical test is whether rebuilding an instance from the same public values should make it interchangeable with the original. If so, a data class usually fits. When lifecycle, resource ownership, or independent identity matters more than property content, an ordinary class is generally more accurate.

## How it works

### The primary constructor sets the boundary

A data class primary constructor must have at least one parameter, and every parameter must be declared with `val` or `var`. A data class can't be `abstract`, `open`, `sealed`, or `inner`. It can implement interfaces and extend an ordinary or sealed class that permits inheritance.

The compiler derives members only from properties in the primary constructor. A property declared in the class body is still instance state, but it is excluded by default from equality, hashing, string output, destructuring, and copying. Moving a property into or out of the primary constructor changes the object's public semantics, not just its layout.

| Declaration site | `equals()` and `hashCode()` | `toString()` | `componentN()` | `copy()` |
| --- | --- | --- | --- | --- |
| Primary-constructor property | Included | Included | Generated in declaration order | Copied as a parameter |
| Class-body property | Excluded | Excluded | Not generated | Initialization runs again |

### Members generated by the compiler

For `data class Parcel(val id: String, val zone: Int)`, the compiler generates `equals()` and `hashCode()` from `id` and `zone`. Equal instances must have the same hash code, allowing them to act as values in a `HashSet` or as keys in a `HashMap`.

The generated `toString()` includes the class name and constructor properties in a form such as `Parcel(id=..., zone=...)`. It is useful for ordinary diagnostic data but isn't a secure audit format. If the constructor holds a token, password, or personal data, logging the instance writes those values too.

The compiler also generates `operator` functions named `component1()`, `component2()`, and so on, following property declaration order. `val (id, zone) = parcel` calls the first two component functions, so the local variable names don't control the mapping. Reversed names still compile but receive the wrong values.

Each `copy()` parameter defaults to the corresponding property on the current instance, and the function invokes the constructor to make a new instance. A named argument replaces only that item. Unspecified references still point to their original objects, so `copy()` is a shallow copy; it doesn't recursively copy nested objects.

### Explicit members and inheritance

You can explicitly implement `equals()`, `hashCode()`, or `toString()` in a data class. Overriding equality without preserving the hashing contract breaks hash-based collections, so design and test `equals()` and `hashCode()` as a pair.

You can't explicitly implement the data class `copy()` or `componentN()` members managed by the compiler. If a supertype supplies open `componentN()` functions with compatible return types, the generated functions override them. An incompatible signature or a `final` member makes the declaration fail to compile.

A data class is final, but final doesn't mean it can't have a superclass. It may extend an open superclass; other classes simply can't extend the data class in turn. If a superclass marks `equals()`, `hashCode()`, or `toString()` as `final`, the data class keeps that implementation instead of generating an override for the member.

### Construction validation and derived state

A data class can validate constructor arguments in an `init` block, and it can declare ordinary methods and computed properties. Because `copy()` invokes construction again, both replaced and retained arguments go through the current validation rules together. Invalid values fail with the same exception whether they come from a direct constructor call or a copy.

A value computed purely from constructor properties usually doesn't need another constructor slot. For example, `fullName` can be a read-only property calculated from `givenName` and `familyName`. It needs no separate role in equality because equal inputs already guarantee an equal result.

Treat caches more carefully. Leaving a mutable cache in the body keeps it outside value semantics, but every `copy()` receives a newly initialized cache. If the cache owns external resources or needs explicit closing, a data class is usually the wrong owner.

Constructor defaults belong to the creation API and also contribute to how `copy()` works. Before adding or changing one, check new construction and copying separately. New instances use the declared default, whereas a copy defaults to the receiving instance's current property.

## Examples

### Value equality, identity, and destructuring

The two `Delivery` instances are created separately but contain the same constructor values. Structural equality compares content, referential equality compares the instances themselves, and destructuring reads properties in declaration order.

<!-- quick -->

```kotlin
data class Delivery(
    val id: String,
    val city: String,
    val priority: Int = 0,
)

fun main() {
    val first = Delivery("D-17", "Paris", priority = 2)
    val sameValue = Delivery("D-17", "Paris", priority = 2)

    println(first)
    println(first == sameValue)
    println(first === sameValue)

    val (id, city, priority) = first
    println("$id -> $city (priority $priority)")
}
```

```text
Delivery(id=D-17, city=Paris, priority=2)
true
false
D-17 -> Paris (priority 2)
```

<!-- /quick -->

`first == sameValue` calls the generated `equals()`, so its result is `true`. `first === sameValue` checks object identity. The two constructor calls make distinct instances, so that result is `false`.

The variables on the final line correspond in order to `component1()`, `component2()`, and `component3()`. If you only need `id`, writing `first.id` is usually more resistant than destructuring all three properties when property order might change later.

### Expressing state changes with `copy()`

Data classes often represent state snapshots. This code replaces both the order status and the item list. `items + "keyboard"` produces a new list, so the old order doesn't observe the added item.

```kotlin
enum class OrderStatus { CREATED, PAID }

data class Order(
    val id: String,
    val status: OrderStatus,
    val items: List<String>,
)

fun main() {
    val created = Order(
        id = "O-204",
        status = OrderStatus.CREATED,
        items = listOf("monitor"),
    )

    val paid = created.copy(
        status = OrderStatus.PAID,
        items = created.items + "keyboard",
    )

    println(created)
    println(paid)
    println(created === paid)
}
```

```text
Order(id=O-204, status=CREATED, items=[monitor])
Order(id=O-204, status=PAID, items=[monitor, keyboard])
false
```

`copy()` always constructs a new outer `Order`; even an argument-free call doesn't return the original instance. Isolation here comes from explicitly creating a new list, not from `copy()` recursively copying anything.

`List` is a read-only collection interface, which means only that this reference exposes no mutation operations. If another `MutableList` reference changes the backing object, the `List` view still observes the change. When you need a stable snapshot, copy at the ownership boundary and constrain element mutability too.

### Observing a shared reference after a shallow copy

Once a data class contains a mutable list, an argument-free `copy()` makes both outer objects share that same list. Mutating the list through the copy also changes what the original instance observes.

```kotlin
data class Team(
    val name: String,
    val members: MutableList<String>,
)

fun main() {
    val original = Team(
        name = "platform",
        members = mutableListOf("Mina", "Noah"),
    )
    val sharedCopy = original.copy()
    sharedCopy.members += "Omar"

    val isolatedCopy = original.copy(
        members = original.members.toMutableList(),
    )
    isolatedCopy.members += "Priya"

    println(original.members)
    println(sharedCopy.members)
    println(isolatedCopy.members)
}
```

```text
[Mina, Noah, Omar]
[Mina, Noah, Omar]
[Mina, Noah, Omar, Priya]
```

`sharedCopy` and `original` hold the same `members` reference, which is why their output lines match. `toMutableList()` copies only the list structure. If the elements themselves are mutable, both lists can still share those element objects.

In production models, prefer exposing collections that consumers don't need to mutate, and make ownership explicit when accepting an external mutable collection. If a mutable copy is genuinely required, define the copy policy at each layer rather than treating `copy()` as a general deep-copy operation.

### Body properties stay outside value semantics

The body property `displayName` doesn't participate in generated members. Two customers compare equal when their `id` values match even if their display names differ. A copied instance also runs the body initializer again.

```kotlin
data class Customer(val id: String) {
    var displayName: String = "anonymous"
}

fun main() {
    val first = Customer("C-8").apply {
        displayName = "Ada"
    }
    val second = Customer("C-8").apply {
        displayName = "Grace"
    }
    val copied = first.copy()

    println(first == second)
    println(first)
    println(first.displayName)
    println(copied.displayName)
}
```

```text
true
Customer(id=C-8)
Ada
anonymous
```

This design is sound only if `displayName` deliberately sits outside customer value semantics. If the business treats it as part of the state snapshot, move it into the primary constructor so equality, string output, and `copy()` use the same boundary.

Body properties can hold caches, derived values, or deliberately excluded runtime state, but they easily create the illusion of a complete copy that silently drops a field. Document the reason for each exclusion and pin the convention down with equality and copy tests.

## Pitfalls

> **Pitfall:** Putting business state in the class body makes generated equality and `copy()` ignore it. Instances with different state can compare equal, and a copy resets the property to its initializer.

**Fix:** Define the object's equality first, then put every piece of state in that definition into the primary constructor. Reserve body properties for caches, derived values, or state that intentionally belongs to an instance rather than its value, and test the exclusion.

> **Pitfall:** `val` only prevents property reassignment, while `List` restricts only the current interface; neither freezes the backing object. Generated code often stores an external `MutableList` and then calls `copy()`, assuming it has made an independent snapshot.

**Fix:** Make collection ownership explicit at model boundaries, copy a collection when a snapshot is required, and inspect whether its elements are mutable too. Prefer read-only types for state models, but don't treat a read-only type as proof of immutability.

> **Pitfall:** After a `var` or nested mutable object used by `equals()` and `hashCode()` changes, a key already stored in a hash table may no longer be found under its new state. The entry remains at its old hash position while lookup uses its changed hash code.

**Fix:** Use a stable, immutable dedicated key for `HashMap` keys and `HashSet` elements. If the model permits editing, remove the old key before the change and insert the new value afterward. Creating a new instance is usually clearer.

> **Pitfall:** `Array.equals()` uses reference semantics, so two data class instances with `Array` properties aren't automatically equal just because the arrays contain equal elements. A `ByteArray` property is particularly easy to mistake for value semantics: independently allocated payloads with identical bytes still compare unequal under the generated data-class implementation.

**Fix:** Prefer `List` for value collections. When an array is required, override both `equals()` and `hashCode()` using matching `contentEquals()` and `contentHashCode()` operations. Nested arrays require their corresponding deep operations.

> **Pitfall:** Generated `toString()` displays every primary-constructor property. Once an access token, password, session identifier, or protected personal value enters a data class, interpolating the whole object into a log can disclose it without explicit field access.

**Fix:** Don't depend on general `toString()` output for models containing secrets. Use a logging structure with an allowlist of fields or an explicit representation that masks sensitive values, and test that raw values never appear in the output.

> **Pitfall:** Destructuring follows `componentN()` positions, not local variable names. Reordering constructor properties can silently swap the meaning of same-typed components while call sites still compile.

**Fix:** Prefer named property access across API boundaries. Destructure only when the positional form is short and stable, and search for every destructuring call site before reordering the properties of a public data class.

<!-- deep -->

## The boundary of value semantics

### Equality and hashing form one contract

Generated `equals()` compares the primary-constructor properties, and generated `hashCode()` uses that same set. A concrete hash value isn't a business identifier to persist across platforms or releases. Depend only on the contract that equal objects have equal hash codes.

Hash-based collections typically use a hash code to locate a candidate position and equality to confirm a key. When a participating key property changes, the collection doesn't automatically relocate an existing entry. That makes a data class with `var` or nested mutable values more prone to exposing this bug than an ordinary state holder.

Before customizing equality, write down its normalization rules. Whether email addresses ignore case or orders compare only by ID is a domain decision the `data` modifier can't make. When equality depends on one stable identity, an ordinary class or dedicated key type is often clearer than overriding a data class's default semantics.

### Copying invokes the construction boundary

Conceptually, `copy()` gives each primary-constructor parameter a `this.property` default and calls the primary constructor. Replaced parameters receive new arguments; unreplaced references pass through unchanged. Initialization blocks, property initializers, and constructor-time validation run again for the new instance.

Consequently, body properties aren't transferred from the old instance; they are initialized again. Nested references don't automatically receive their own `copy()` calls either. To update nested immutable state, call `copy()` explicitly at each level. To duplicate a mutable object graph, define ownership and a copying rule for every layer.

Default parameters keep call sites focused on changes, but they can also hide newly added properties. When a data class gains a constructor parameter with a default, old `copy()` calls preserve that property's value from the current instance. That is usually right, but serialization, persistence, and state migration can require a different default policy.

### Component order is a source interface

Each `componentN()` corresponds to one primary-constructor property, with its number determined by declaration order. Names in a destructuring declaration are only new local names; they aren't matched to property names. When `_` skips an item, Kotlin doesn't call that component function.

Reordering properties in a public data model changes positional arguments and destructuring results. Named constructor arguments protect a caller from the first change but not the second. Keeping destructuring local and short reduces this positional coupling.

### An array is not a collection value

Kotlin arrays are specialized objects whose ordinary `equals()` doesn't recursively compare elements. Generated data class equality delegates to each property's own equality, so it doesn't add content semantics to an array. `contentEquals()` compares one level by corresponding index, while `contentDeepEquals()` handles nested arrays.

Hashing must use a rule at the same depth. Pair `contentEquals()` with `contentHashCode()`, or pair `contentDeepEquals()` with `contentDeepHashCode()`. Changing comparison without hashing can place equal objects in different hash positions and violate the contract collections rely on.

### A data class isn't an immutability declaration

`data` generates members and `val` fixes a property reference; neither controls methods on the referenced object. A data class containing only `val` properties can still hold a `MutableList`, a mutable service object, or an external buffer. True immutability requires no observable mutation path from the root object through any reachable state.

A read-only collection interface can reduce the current caller's capabilities but can't prevent another alias from changing the backing collection. A stable snapshot requires independent data at the boundary and no leaked mutable alias. If the elements are mutable objects, copying only the collection structure is still insufficient.

### Choosing a neighboring representation

A data class isn't the default answer for every small class. Decide whether the object needs value, identity, or singleton semantics before choosing its declaration.

| Requirement | Typical choice | Main reason |
| --- | --- | --- |
| Several properties jointly define one value | Data class | Generates the complete set of value members |
| Lifecycle or stable ID distinguishes an object | Ordinary class | Keeps mutable fields out of equality by default |
| One underlying value needs a distinct static type | Value class | Constrains wrapping semantics and representation cost |
| A payload-free singleton in a closed state set | `data object` | Gives a singleton stable data-style output |

DTOs often fit data classes because they usually represent value snapshots at a boundary. Transfer fields, domain equality, and persistence identity aren't necessarily the same, however. Don't assume every field belongs to one value definition merely because a database row or JSON object has fields.

A long-lived service with extensive internal state usually fits an ordinary class. Its connection pools, caches, and collaborators aren't data to compare property by property. Declaring the service as a data class creates misleading equality and log output.

When a type has one underlying value, compare a data class with a value class. Value classes have distinct boxing and identity rules and are useful for separating meanings that share an underlying type. Data classes fit values that need several components, destructuring, or an ordinary object representation.

Compare a payload-free closed state with `data object` rather than inventing a data class with no meaningful payload. State branches that carry data can remain data classes and combine with a sealed class or interface; the hierarchy itself belongs to the sealed-types topic.

### Evolving a public data class

A public data class primary constructor simultaneously affects construction, equality, hashing, output, component order, and copying. Its change surface is larger than that of a private plain carrier. A review can't stop after checking whether constructor calls still compile.

Appending a property with a default often lets existing named calls compile, but it changes equality, hashing, and string output. Snapshot tests, cache keys, and log processing can all change. A default solves call compatibility, not semantic compatibility.

Reordering same-typed properties is particularly dangerous. Positional arguments and destructuring can keep compiling while sending values to the wrong names. Public APIs should encourage named arguments and treat destructuring as a separate call form to search for.

Removing a property removes a component function and renumbers every later component. Even if all project source is recompiled, callers can still have faulty positional logic. Cross-module binary compatibility requires dedicated tooling and can't be inferred from passing source tests.

Moving a property from the primary constructor to the body isn't a behavior-preserving refactor. It removes that property from five generated-member categories at once. Moving a body property into the constructor expands equality and log output instead, potentially exposing state that was previously excluded.

Record the old contract before evolving the type: which instances compare equal, which fields can appear in logs, which references a copy should share, and which callers destructure it. Retesting the same contract table after the change separates intentional changes from regressions.

### Testing generated semantics

A data class contains little handwritten code, but its generated behavior still needs tests. The goal isn't to prove the compiler generates members. It is to prove your chosen constructor boundary matches the domain contract.

| Test axis | Minimal counterexample |
| --- | --- |
| Equality | Two independent instances that differ in one property |
| Hash stability | Every permitted mutation after collection insertion |
| Copy isolation | Mutation of each nested mutable value through the copy |
| Body exclusion | Different body properties followed by comparison and copying |
| Output safety | A sentinel secret checked against every log representation |

Equality tests need both positive and negative cases. Merely asserting that two identical instances are equal can't reveal a required property accidentally left in the body. Change each value-defining property one at a time to verify that the boundary is complete.

Hash tests should perform real `HashSet` or `HashMap` operations instead of just comparing two `hashCode()` results. Insert first, perform every state change the model permits, then look up and remove the key to expose stability failures.

Copy tests should begin with reference aliases. For every list, array, or nested object, decide whether the copy should share it, copy its container, or recursively copy its elements, then verify the decision by mutating both sides.

Output tests can place a unique sentinel string in a sensitive field and check `toString()` plus structured logging output. If a later edit adds a log call or changes the field representation, the test then fails with the specific leaked value.

<!-- /deep -->

[Checkpoint: kotlin/data-classes](https://codewiki.com/kotlin/data-classes/#checkpoint)

## Further reading

These are the maintained source pages behind Kotlin's official language guide. The equality page also links to the standard-library array content operations used in the pitfall above.

- [Kotlin documentation source: Data classes](https://raw.githubusercontent.com/JetBrains/kotlin-web-site/master/docs/topics/data-classes.md)
- [Kotlin documentation source: Equality](https://raw.githubusercontent.com/JetBrains/kotlin-web-site/master/docs/topics/equality.md)
- [Kotlin documentation source: Destructuring declarations](https://raw.githubusercontent.com/JetBrains/kotlin-web-site/master/docs/topics/destructuring-declarations.md)
