Private Data Class Copy

ID

kotlin.private_data_class_copy

Severity

low

Remediation Complexity

trivial

Remediation Risk

low

Remediation Effort

low

Resource

Reliability

Language

Kotlin

Tags

best-practice, reliability

Description

Reports a data class declared with a private constructor. The compiler generates a copy() member whose visibility matches the class, not the primary constructor. Making the constructor private therefore only stops direct instantiation — clients can still produce new instances via copy(), bypassing the intended restriction.

Rationale

data class User private constructor(val id: Int) {
    companion object { fun create(id: Int) = User(id) }
}

fun caller(u: User) {
    val other = u.copy(id = 99)  // public copy() side-steps `private constructor`
}

The private constructor is usually applied to enforce invariants — only the companion factory should produce valid instances. The auto-generated copy() quietly defeats that goal: any caller holding an existing instance can fabricate a fresh one with arbitrary field values.

Remediation

// Option 1: drop `data` and write equality manually
class User private constructor(val id: Int) {
    companion object { fun create(id: Int) = User(id) }
    // implement equals/hashCode/toString as needed
}

// Option 2: keep `data` but make the class itself non-public
internal data class User(val id: Int)

Either remove the data modifier and write the desired override manually, or make the data class itself internal / private so that copy() cannot be called by external code. From Kotlin 2.0 onward, exposing copy() from a non-public primary constructor will require an explicit opt-in.