Override getter for Kotlin data class

After spending almost a full year of writing Kotlin daily I've found that attempting to override data classes like this is a bad practice. There are 3 valid approaches to this, and after I present them, I'll explain why the approach other answers have suggested is bad.

  1. Have your business logic that creates the data class alter the value to be 0 or greater before calling the constructor with the bad value. This is probably the best approach for most cases.

  2. Don't use a data class. Use a regular class and have your IDE generate the equals and hashCode methods for you (or don't, if you don't need them). Yes, you'll have to re-generate it if any of the properties are changed on the object, but you are left with total control of the object.

    class Test(value: Int) {
      val value: Int = value
        get() = if (field < 0) 0 else field
    
      override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (other !is Test) return false
        return true
      }
    
      override fun hashCode(): Int {
        return javaClass.hashCode()
      }
    }
    
  3. Create an additional safe property on the object that does what you want instead of having a private value that's effectively overriden.

    data class Test(val value: Int) {
      val safeValue: Int
        get() = if (value < 0) 0 else value
    }
    

A bad approach that other answers are suggesting:

data class Test(private val _value: Int) {
  val value: Int
    get() = if (_value < 0) 0 else _value
}

The problem with this approach is that data classes aren't really meant for altering data like this. They are really just for holding data. Overriding the getter for a data class like this would mean that Test(0) and Test(-1) wouldn't equal one another and would have different hashCodes, but when you called .value, they would have the same result. This is inconsistent, and while it may work for you, other people on your team who see this is a data class, may accidentally misuse it without realizing how you've altered it / made it not work as expected (i.e. this approach wouldn't work correctly in a Map or a Set).


You could try something like this:

data class Test(private val _value: Int) {
  val value = _value
    get(): Int {
      return if (field < 0) 0 else field
    }
}

assert(1 == Test(1).value)
assert(0 == Test(0).value)
assert(0 == Test(-1).value)

assert(1 == Test(1)._value) // Fail because _value is private
assert(0 == Test(0)._value) // Fail because _value is private
assert(0 == Test(-1)._value) // Fail because _value is private
  • In a data class you must to mark the primary constructor's parameters with either val or var.

  • I'm assigning the value of _value to value in order to use the desired name for the property.

  • I defined a custom accessor for the property with the logic you described.


The answer depends on what capabilities you actually use that data provides. @EPadron mentioned a nifty trick (improved version):

data class Test(private val _value: Int) {
    val value: Int
        get() = if (_value < 0) 0 else _value
}

That will works as expected, e.i it has one field, one getter, right equals, hashcode and component1. The catch is that toString and copy are weird:

println(Test(1))          // prints: Test(_value=1)
Test(1).copy(_value = 5)  // <- weird naming

To fix the problem with toString you may redefine it by hands. I know of no way to fix the parameter naming but not to use data at all.