Data Object Kotlin vs Regular Object: Key Differences and Usage Guide
A data object in Kotlin is a singleton that automatically generates toString(), equals(), and hashCode() methods like a data class, while a regular object requires manual implementation of these methods.
The Kotlin compiler treats data objects as a distinct language feature that bridges the gap between singleton objects and data classes. According to the JetBrains/kotlin repository source code, data objects provide built-in structural equality and string representation without the boilerplate code required by regular objects.
What Is a Data Object in Kotlin?
A data object is a singleton declaration that combines the single-instance semantics of an object with the automatic method generation of a data class. When you declare data object Singleton, the compiler generates implementations for toString(), equals(), and hashCode() automatically, whereas a regular object declaration generates none of these methods by default.
This feature is particularly useful when you need a singleton to represent a specific data value or state within your application, such as a unique configuration instance or a sealed class variant.
Generated Members: toString, equals, and hashCode
The primary distinction between data objects and regular objects lies in compiler-generated members. In compiler/frontend/src/org/jetbrains/kotlin/resolve/checkers/DataObjectContentChecker.kt, the compiler enforces that data objects receive automatic implementations of standard data class methods.
For a regular object, you must manually override these methods:
object RegularConfig {
val name = "default"
override fun toString() = "RegularConfig(name=$name)"
override fun equals(other: Any?) = other === this
override fun hashCode() = System.identityHashCode(this)
}
A data object generates these automatically:
data object DataConfig {
val name = "default"
}
// Compiler generates:
// toString() returns "DataConfig"
// equals() compares instance identity (always true for singleton)
// hashCode() based on instance identity
Restrictions on Custom equals and hashCode
Unlike regular objects, data objects cannot override equals() or hashCode(). The DataObjectContentChecker in the Kotlin compiler explicitly prohibits these overrides to maintain consistent structural equality semantics.
If you attempt to override these methods in a data object, the compiler generates the error DATA_OBJECT_CUSTOM_EQUALS_OR_HASH_CODE:
// This code will fail to compile
data object InvalidObject {
override fun equals(other: Any?) = true // ERROR: DATA_OBJECT_CUSTOM_EQUALS_OR_HASH_CODE
override fun hashCode() = 42 // ERROR: DATA_OBJECT_CUSTOM_EQUALS_OR_HASH_CODE
}
This restriction ensures that all data objects behave consistently regarding equality, relying on the compiler-generated implementations that treat the singleton instance as the sole representative of that type.
Reflection and Type Identification
Data objects are identified as data classes through Kotlin reflection. According to libraries/stdlib/jvm/src/kotlin/reflect/KClass.kt, the isData property returns true for data objects, allowing runtime identification of these special singletons.
data object SingletonData
object RegularSingleton
println(SingletonData::class.isData) // true
println(RegularSingleton::class.isData) // false
This distinction is crucial for serialization libraries, reflection-based frameworks, and generic code that needs to handle data classes and data objects uniformly while treating regular objects differently.
Data Objects in Sealed Hierarchies
One of the most powerful applications of data objects appears in sealed class hierarchies. When used as variants of a sealed class or interface, data objects provide exhaustiveness checking in when expressions while maintaining the singleton semantics required for state representation.
The compiler test compiler/fir/analysis-tests/testData/resolve/exhaustiveness/negative/exhaustiveWithNegativeSealedDataObjects.kt demonstrates how the Kotlin compiler treats data objects as complete sealed variants for exhaustiveness analysis.
sealed interface NetworkState
data object Loading : NetworkState
data object Error : NetworkState
data class Success(val data: String) : NetworkState
fun handleState(state: NetworkState) = when (state) {
is Loading -> "Loading data..."
is Error -> "An error occurred"
is Success -> "Received: ${state.data}"
// No else branch needed - exhaustive because Loading and Error are data objects
}
Unlike regular objects, data objects in sealed hierarchies signal to the compiler and readers that these variants represent distinct data states rather than behavioral implementations, improving type safety and code clarity.
Language Feature Requirements
Data objects require explicit language feature support. According to compiler/util/src/org/jetbrains/kotlin/config/LanguageVersionSettings.kt, the DataObjects language feature must be enabled for the compiler to recognize and process data object declarations.
This feature is available in Kotlin 1.9.0 and later versions. When using data objects in your projects, ensure your Kotlin compiler version supports this feature and that it is enabled in your build configuration.
Summary
- Data objects automatically generate
toString(),equals(), andhashCode()methods, while regular objects require manual implementation of these methods. - The Kotlin compiler prohibits overriding
equals()orhashCode()in data objects throughDataObjectContentChecker.ktto maintain consistent equality semantics. - Data objects return
trueforKClass.isDatareflection checks, identifying them as data classes at runtime. - When used in sealed hierarchies, data objects provide exhaustiveness checking in
whenexpressions without requiring anelsebranch. - The
DataObjectslanguage feature must be enabled inLanguageVersionSettings.ktto use this functionality, available from Kotlin 1.9.0 onwards.
Frequently Asked Questions
Can I add properties to a data object?
Yes, you can declare properties inside a data object just like a regular object. However, these properties do not participate in the generated equals() or hashCode() implementations because a data object is a singleton with only one instance. The generated toString() returns only the object name, not its property values, unlike data classes which include properties in their string representation.
Why does my data object not show property values in toString()?
The Kotlin compiler generates a simplified toString() for data objects that returns only the object's name, as verified in compiler/testData/codegen/box/dataObjects/toString.kt. This differs from data classes, which generate toString() implementations that include all primary constructor properties. If you need property values in the string representation, you must override toString() manually, though you cannot override equals() or hashCode().
How do data objects improve sealed class hierarchies?
Data objects enhance sealed class hierarchies by providing clear, exhaustive variant representation without state variation. According to the exhaustiveness tests in compiler/fir/analysis-tests/testData/resolve/exhaustiveness/negative/exhaustiveWithNegativeSealedDataObjects.kt, the compiler recognizes data objects as complete sealed variants, enabling exhaustive when expressions without else branches. This improves type safety and makes the code's intent clearer—signaling that the variant represents a distinct state rather than a behavioral service.
What happens if I try to override equals in a data object?
The Kotlin compiler will reject your code with the error DATA_OBJECT_CUSTOM_EQUALS_OR_HASH_CODE. The check is implemented in compiler/frontend/src/org/jetbrains/kotlin/resolve/checkers/DataObjectContentChecker.kt, which explicitly prohibits custom equals() and hashCode() overrides in data objects. This restriction ensures that all data objects maintain consistent identity-based equality semantics, treating the singleton instance as the sole representative of that type.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →