I have encountered a problem while trying to resolve problem with sensitive info in application logs — we have quite large data class object that at some time in processing flow we log. Problem is that this object contains properties email
and phoneNumber
, which we need to mask while logging to avoid leaking this data, for this we have two extension methods: String.maskEmail()
and String.maskPhoneNumber()
.
That was my first solution, during which I encountered StackOverflowError:
data class Foo(
val email: String = "email",
val phoneNumber: String = "phoneNumber",
// consider the fact that there is tens more props in real object
) {
override fun toString() = withMaskedProps
private val withMaskedProps: String = copy(
email = email.maskEmail(), phoneNumber = phoneNumber.maskPhone(),
).toString()
}
In hindsight it's obvious, because object at initialization recursively creates the copy, which creates the copy and etc. until we get the error.
The question is — can I achieve desired outcome without SO? I need the copied object to simply access property fields, without initializing anything else to not cause SO. And I want to make it universal, by overriding toString()
and by ensuring that I don't need to support new fields in string representation if new properties will be added to the data class.
Overriding toString
in a data class necessarily means that you are giving up the automatically generated implementation. If you don't want to do that, you can create your own PhoneNumber
and Email
classes that returns masked values from their toString
.
@JvmInline
value class PhoneNumber(val value: String) {
override fun toString() = "XXXX XXXX"
}
@JvmInline
value class Email(val value: String) {
override fun toString() = "example@example.com"
}
data class Foo(
val email: Email,
val phoneNumber: PhoneNumber,
// ...
)
I have made these value class
es so that they get inlined on JVM, but they can also be regular classes or data classes if you prefer.