javakotlinstack-overflowdata-class

How to make data class copy with only properties initialized, to avoid StackOverflowError?


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.


Solution

  • 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 classes so that they get inlined on JVM, but they can also be regular classes or data classes if you prefer.