skip to content

What happens to `copy()` and `componentN()` if a data class's primary-constructor property is private, and what visibility pitfalls follow?

level: seniorimportance: nice to knowfreq 25%

answer

  1. private ctor prop still gets copy()/componentN()
  2. generated copy() is public
  3. copy() is a back door to set private state
  4. private constructor still leaks via copy()
  5. Use plain class + factory for invariants

basics

~10 s

The generated copy() and componentN() still cover every constructor property, including private ones, and copy() itself is public. So a private property can leak out through copy(), which can undermine your intended encapsulation.

solid answer

~40 s

Even when a primary-constructor property is `private`, it is still a constructor property, so `copy()` and `componentN()` are generated for it. The generated `copy()` is `public` and its parameter list includes the private property, which means callers outside the class can supply a new value for a property they otherwise cannot read or write directly. This is a real encapsulation leak: `copy(...)` becomes a back door to mutate 'private' state, and destructuring via `component1()` can expose it positionally. Strategies: don't rely on data classes for strong encapsulation of constructor state; make the whole data class `internal`/`private`; or model invariant-guarded types as regular (non-data) classes with a private constructor plus a factory. Kotlin does not let you change the visibility of an individual generated member.

code

kotlin · 8 lines
kotlin
data class Token(val id: Int, private val secret: String)

fun main() {
    val t = Token(1, "abc")
    // val s = t.secret        // ERROR: secret getter is private
    val forged = t.copy(secret = "hacked") // OK from outside!
    println(forged)            // Token(id=1, secret=hacked)
}

go deeper

for a junior

Recognizes that private only affects direct access, and that copy() still exists.

for a middle

Knows copy() includes all constructor properties and is public, exposing private state.

for a senior

Explains the encapsulation leak end-to-end and proposes class+factory alternatives for invariant-guarded types.

for a principal

Frames the tension between transparent value semantics and encapsulation as a design choice, and sets team conventions on when data classes are inappropriate.

## The core surprise Marking a primary-constructor property `private` does **not** remove it from the generated members. It is still a constructor property, so: - `copy()` includes it as a parameter. - `componentN()` is generated for it. - `equals`/`hashCode`/`toString` still consider it (though `toString`'s formatting still prints it). And crucially, the generated `copy()` is **public** by default. ```kotlin data class Token(val id: Int, private val secret: String) fun leak(t: Token): Token = t.copy(secret = "override") // compiles from outside! ``` Here `secret` is `private`, yet an external caller can pass a new `secret` to `copy()`. The property's *getter* is private (you can't read `t.secret` outside), but the `copy()` parameter lets you *set* it on a new instance. ## Why this happens The compiler generates one `copy()` covering **all** constructor properties to make `copy()` a faithful 'reconstruct with changes' operation. Generating it any other way would make `copy()` unable to round-trip the object. Kotlin offers no syntax to give an individual generated member a narrower visibility. ## Destructuring exposure Because `component1()`, `component2()`, … are generated positionally for all constructor properties, `val (a, b) = token` can pull out values; whether the *destructured name* is usable depends on the component visibility, but the positional `componentN` functions exist for private props too. ## Mitigations 1. **Don't use data classes for strongly-encapsulated invariants.** If a type must guard invariants (e.g., a validated email), prefer a regular `class` with a `private constructor` and a companion `operator fun invoke`/factory: ```kotlin class Email private constructor(val value: String) { companion object { fun of(raw: String): Email? = if (raw.contains('@')) Email(raw) else null } } ``` 2. **Restrict the whole type's visibility** — make the data class `internal` or `private` so the public `copy()` is not part of your public API. 3. **Accept it** for simple DTOs where the property is private only to discourage casual access, not to enforce security. ## Related: private primary constructor You can write `data class P private constructor(val x: Int)`. This blocks direct construction, but the generated `copy()` is still public and can construct new instances from an existing one — another subtle leak to be aware of. ## Takeaway Data classes optimize for transparent value semantics, which is fundamentally at odds with strong encapsulation of their constructor state. Choose data classes for transparent value holders, and plain classes + factories for invariant-protected types.

  • Can you make the generated copy() private or internal directly?
    No. Kotlin provides no way to set the visibility of an individual generated member; you can only narrow the whole class's visibility or stop using a data class.
  • How would you model a validated value object that truly hides its constructor?
    Use a regular class with a private constructor and a factory (companion function) that enforces invariants and returns null/Result on failure — not a data class.

Marking a constructor property private is like locking the front door but leaving copy() as an unlocked back door that hands out a duplicate house with any locks you choose.

saying these in an interview costs you the question

  • Believing private constructor properties are excluded from copy()
  • Assuming the generated copy() respects each property's visibility
  • Using a data class to enforce invariants on hidden state
  • Thinking a private primary constructor prevents reconstruction via copy()
  • Claiming you can annotate copy() to make it private

context