Question
Given this Kotlin data class:
data class Test(val value: Int)
How can the value getter return 0 when the stored value is negative? If overriding the getter directly is not possible, what Kotlin techniques can provide an appropriate result?
Short Answer
You will learn why a primary-constructor property in a Kotlin data class cannot declare a custom getter, how to expose a calculated property with a private backing value, and when it is better to normalize or reject invalid input instead.
Concept
A Kotlin property can have a custom getter:
val value: Int
get() = 0
However, a property declared directly in a data class's primary constructor has a compact form:
data class Test(val value: Int)
That syntax creates both a constructor parameter and a property, but it does not provide a place to write a custom accessor. You also cannot declare another property named value in the class body.
Additionally, override is not the right keyword here. override is used only when a class replaces an open or abstract property inherited from a superclass or interface. A custom getter changes the behavior of a property declared by the same class.
The important design decision is whether negative numbers should be:
- Stored but hidden from callers: retain the original value internally and expose
0through a computed getter. - Normalized when the object is created: store
0instead of every negative value. - Rejected: fail early because a negative value is invalid for the domain.
This choice affects equality, copy(), destructuring, serialization, and debugging.
Mental Model
Think of a property as a service desk.
- A normal stored property gives visitors the item from a box directly.
- A custom getter lets the desk inspect the item before handing it over.
- A backing property is the box in the staff-only room.
If the box contains -5, the service desk can hand out 0. But the original box still contains -5. Alternatively, you can replace -5 with 0 before putting anything in the box. These two approaches look similar to a caller reading value, but they have different behavior elsewhere.
Syntax and Examples
A custom getter is declared in a property body:
val displayedValue: Int
get() = rawValue.coerceAtLeast(0)
For a data class, put the stored value in the primary constructor and expose a separate computed property:
data class Test(private val rawValue: Int) {
val value: Int
get() = rawValue.coerceAtLeast(0)
}
fun main() {
val test = Test(-8)
println(test.value) // 0
}
coerceAtLeast(0) returns the number itself when it is at least 0; otherwise, it returns 0.
An equivalent if expression is:
val value: Int
get() = if (rawValue < 0) 0 else rawValue
Use coerceAtLeast when the rule is simply “never expose a value below this minimum.”
Step by Step Execution
Consider this code:
data class Test(private val rawValue: Int) {
val value: Int
get() = rawValue.coerceAtLeast(0)
}
fun main() {
val first = Test(12)
val second = Test(-4)
println(first.value)
println(second.value)
}
Execution steps:
Test(12)stores12infirst.rawValue.- Reading
first.valueruns its getter. 12.coerceAtLeast(0)evaluates to12, so the first line prints12.Test(-4)stores-4insecond.rawValue.- Reading
second.valueruns the same getter. (-4).coerceAtLeast(0)evaluates to0, so the second line prints0.
The getter runs every time value is read. It does not change rawValue.
Real World Use Cases
Custom or computed getters are useful when an application needs a safe value for display or calculations:
- Progress indicators: show at least
0%even if an intermediate calculation becomes negative. - Inventory displays: prevent a UI from displaying a negative available quantity while an adjustment is pending.
- Pricing previews: expose a non-negative charge after applying discounts.
- API response models: provide a derived, client-friendly field from a raw imported value.
- Compatibility layers: keep legacy raw data but present a corrected interpretation to newer code.
For business-critical rules, consider whether hiding invalid data is appropriate. A negative stock count may be valuable information that should be fixed rather than silently displayed as zero.
Real Codebase Usage
In real Kotlin projects, developers commonly choose one of these patterns.
1. Normalize at construction time
Use this when negative values have no meaning anywhere in the program:
data class Test(val value: Int) {
init {
require(value >= 0) { "value must not be negative" }
}
}
Or, when clamping is the intended rule:
data class Test private constructor(val value: Int) {
companion object {
operator fun invoke(value: Int): Test {
return Test(value.coerceAtLeast(0))
}
}
}
This keeps data-class equality, copy(), and destructuring aligned with the public value.
2. Keep raw data and expose a derived value
Use this when the original value matters for diagnostics, auditing, or a separate calculation:
data class Test(private val rawValue: Int) {
val value: Int get() = rawValue.coerceAtLeast(0)
}
3. Use a named derived property
Often the clearest approach is to avoid calling two different concepts value:
data class Test(val rawValue: Int) {
val nonNegativeValue: Int
get() = rawValue.coerceAtLeast(0)
}
Clear names make it obvious whether code is using original input or a safe display value.
Common Mistakes
Trying to add a getter after a primary-constructor property
This is not valid Kotlin:
// Does not compile
data class Test(val value: Int) {
get() = value.coerceAtLeast(0)
}
A getter must belong to a property declaration, and the compact primary-constructor property syntax cannot include an accessor body.
Redeclaring the same property name
This also does not compile:
// Does not compile
data class Test(val value: Int) {
val value: Int
get() = 0
}
The class would contain two properties named value.
Creating infinite recursion in a getter
This compiles but crashes with a stack overflow when read:
class Test(private val rawValue: Int) {
val value: Int
get() = value.coerceAtLeast(0) // Reads value again and again
}
Read the backing property instead:
val value: Int
get() = rawValue.coerceAtLeast(0)
Assuming data-class equality uses computed body properties
data class Test(private val rawValue: Int) {
val value: Int get() = rawValue.coerceAtLeast(0)
}
println(Test(-1) == Test(-2)) // false
Both objects expose value == 0, but the data class compares its primary-constructor property, rawValue. Normalize the stored constructor value if equality should treat both as the same.
Using override without inheritance
A getter is not “overridden” unless it replaces an inherited open property. For a property declared in the same class, write a custom getter without override.
Comparisons
| Approach | What is stored? | What callers read? | Best when |
|---|---|---|---|
Custom getter with rawValue | Original value, including negatives | 0 for negatives | Raw input must be retained |
| Clamp on construction | Only non-negative value | Stored value | Negative values should never exist in the model |
require(value >= 0) | Only valid values | Stored value | Negative input is a programming or validation error |
| Named derived property | Original value | A separate safe value | Both raw and adjusted meanings are important |
A data class only includes properties declared with val or var in its primary constructor in generated equals(), hashCode(), copy(), toString(), and componentN() functions. Properties declared in the class body are not included.
Cheat Sheet
// A regular custom getter
val property: Int
get() = expression
// Data class with a private stored value and public computed value
data class Test(private val rawValue: Int) {
val value: Int
get() = rawValue.coerceAtLeast(0)
}
// Keep the data class value canonical by clamping before storage
data class Test private constructor(val value: Int) {
companion object {
operator fun invoke(value: Int) = Test(value.coerceAtLeast(0))
}
}
// Reject invalid input
data class Test(val value: Int) {
init {
require(value >= 0)
}
}
Rules:
overriderequires an inheritedopenor abstract property.- Do not read the same property inside its own getter.
- Primary-constructor data-class properties determine generated equality and
copy()behavior. coerceAtLeast(0)is a concise non-negative clamp.
FAQ
Can a Kotlin data class primary-constructor property have a custom getter?
Not with the compact data class Test(val value: Int) syntax. Use a separate backing property and a body property, or normalize the constructor input before storing it.
Does coerceAtLeast(0) change the original integer?
No. Int values are immutable. The function returns a new result: either the original number or 0.
Should I clamp a negative value or throw an exception?
Clamp when negative input is expected and 0 is the defined fallback. Use require when a negative value indicates invalid program state or invalid input that must be corrected.
Will copy() use a computed property in a data class?
No. copy() uses primary-constructor properties. With private val rawValue, copy() copies rawValue, not the computed value property.
Why are Test(-1) and Test(-2) unequal when both return zero?
When the data class stores rawValue, equality compares rawValue. Normalize before storage if both inputs should be considered equivalent.
Can an extension property replace an existing getter?
No. Extension properties cannot override or replace member properties. They also cannot have backing fields.
Is maxOf(value, 0) equivalent to value.coerceAtLeast(0)?
For this integer rule, yes. coerceAtLeast(0) communicates the intent more directly.
Mini Project
Description
Build a small score model for a quiz application. Imported score adjustments may be negative, but the displayed score must never be below zero. The model should retain the raw adjustment so a developer can inspect it later.
Goal
Create a Kotlin data class that exposes a non-negative score while preserving the original raw score.
Requirements
Define a data class with a private primary-constructor property named rawScore.
Add a public read-only property named displayScore.
Make displayScore return 0 when rawScore is negative.
Create one positive and one negative instance.
Print each instance's raw representation and displayed score.
Keep learning
Related questions
Accessing Kotlin Extension Functions from Java
Learn how Kotlin extension functions are compiled and how to call them correctly from Java with clear examples and common pitfalls.
Add a Maven Repository URL in Gradle Kotlin DSL (build.gradle.kts)
Learn how to add Maven Central and a custom Maven repository URL in Gradle Kotlin DSL using build.gradle.kts syntax.
Allow HTTP and HTTPS in Android 9 Pie with Network Security Configuration
Learn how Android 9 Pie handles cleartext HTTP traffic and how to allow HTTP and HTTPS safely using network security config.