Reputation:
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.
Upvotes: 0
Views: 100
Reputation: 273540
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() = "[email protected]"
}
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.
Upvotes: 0
Reputation: 1813
Change withMaskedProps
to not be a property initialized at init.
Make it a getter. It will generate the copy only when withMaskedProps
is called.
private val withMaskedProps: String get() = copy(...).toString()
Or make it lazy. It will generate the copy once, only if withMaskedProps
is accessed.
private val withMaskedProps: String by lazy { copy(...).toString() }
Upvotes: -1
Reputation: 9952
If I understand correctly, your requirement is really to modify the behaviour of the toString()
function. You want it to behave mostly as normal, but with some properties modified or redacted.
The toString()
function of a data class is generated for you by the compiler. Unfortunately, there's no way to override toString()
while still retaining access to the compiler's generated version. When the compiler sees that you've provided your own implementation, it will skip adding its own.
That means you can't really "mix and match" in the way that you want. As you've discovered, making a copy()
will let you redact the properties, but you still need to override toString()
to insert that change. At that point, the original generated implementation of toString()
is gone, and can't be retrieved.
You can get around it by modifying the compiler's generated toString()
implementation, instead of adding an override. To do that, you'll need a compiler plugin. There are at least two plugins designed for this purpose.
For example, using https://github.com/ZacSweers/redacted-compiler-plugin, you could annotate your class or its properties as @Redacted
. There's no need to create a copy()
.
@Retention(SOURCE)
@Target(PROPERTY, CLASS)
annotation class Redacted
data class Foo(
@Redacted val email: String = "email",
@Redacted val phoneNumber: String = "phoneNumber",
…
)
The plugin has plenty of configuration options to let you customize how the redacted data will appear.
There's also a similar https://github.com/aafanasev/sekret plugin.
Upvotes: 1