skip to content

What risks does the auto-generated `toString()` of a data class introduce in production logging, and how do you mitigate them?

level: middleimportance: should knowfreq 45%

answer

  1. toString dumps ALL constructor props
  2. Logging leaks secrets/PII/tokens
  3. Override toString to mask
  4. Or wrap secret in redacting value class
  5. Masking doesn't touch equals/hashCode

basics

~10 s

The generated toString prints every constructor property, including secrets like passwords or tokens. If you log a data object, those values can leak into logs. You should override toString to mask sensitive fields.

solid answer

~30 s

A data class's generated `toString()` lists all primary-constructor properties verbatim (`User(name=Ann, password=hunter2)`). Logging the object — directly or via a logger's string interpolation — leaks any secret, PII, or token it holds. Mitigations: override `toString()` to mask sensitive fields, keep secrets out of the primary constructor (declare them in the body so they're excluded from the generated form), wrap secrets in a value type whose own `toString` redacts, or avoid logging whole DTOs. Note overriding `toString` doesn't affect `equals`/`hashCode`, so masking the printed form is safe and orthogonal to equality.

code

kotlin · 6 lines
kotlin
@JvmInline value class Token(val raw: String) {
    override fun toString() = "***"
}
data class Session(val userId: Long, val token: Token)

println(Session(7, Token("abc.def")))  // Session(userId=7, token=***)

go deeper

for a junior

Recognizes the generated toString prints all fields including secrets.

for a middle

Knows concrete mitigations: override toString, body property, or redacting wrapper type.

for a senior

Designs a redacting value type and explains masking is orthogonal to equality.

for a principal

Sets org-wide conventions for logging DTOs, PII handling, and reviewing toString on sensitive types.

## The risk The generated `toString()` is a faithful dump of **every primary-constructor property**: `User(name=Ann, password=hunter2, token=eyJ...)`. Because logging frameworks call `toString()` on interpolated objects, a single `log.info("login $user")` can write credentials, PII, or tokens to log files, log aggregators, and crash reports. ## Mitigations **1. Override `toString()` to mask:** ```kotlin data class Credentials(val user: String, val password: String) { override fun toString() = "Credentials(user=$user, password=***)" } ``` This keeps generated `equals`/`hashCode` intact (overriding one member doesn't suppress the others). **2. Keep secrets out of the primary constructor:** Declare the secret as a body property so it's excluded from the generated `toString` (and from `equals`/`hashCode`). Trade-off: it's then excluded from equality too. **3. Wrap the secret in a redacting type:** ```kotlin @JvmInline value class Secret(val value: String) { override fun toString() = "***" } data class Credentials(val user: String, val password: Secret) ``` Now even the generated `toString` prints `password=***` because it calls `Secret.toString()`. **4. Don't log whole DTOs:** log only the fields you need, or use a structured logger with explicit field selection. ## Why it's easy to miss The convenience of `data class` is exactly what makes it dangerous: you get a detailed `toString` for free and tend to log objects wholesale. Treat any data class that carries credentials/PII as needing an explicit, reviewed `toString`. ## Relationship to equals/hashCode Masking only changes the printed form. `equals`/`hashCode` still use the real values, so authentication and lookups keep working — masking and equality are orthogonal.

  • If you override toString to mask a field, does equality still use the real value?
    Yes. Overriding toString doesn't change the generated equals/hashCode, which still compare the actual property values.
  • How can you exclude a secret from both toString and equality?
    Declare it as a body property instead of a constructor property; body properties are excluded from all generated members.

A data class toString is like a receipt that prints your full card number — handy until you leave it on the counter.

saying these in an interview costs you the question

  • Assumes toString hides sensitive fields by default
  • Logs entire DTOs without considering generated toString content
  • Thinks masking toString breaks equals/hashCode
  • Uses @Transient expecting it to redact toString (it's for serialization)

context