Question

Can You Extend a Data Class in Kotlin? Inheritance, Limits, and Better Alternatives

kotlininheritanceabstractdata-class

Question

In Kotlin, I want to use inheritance with a data class, similar to how I might extend a plain Java POJO.

For example, I would like to write code like this:

open data class Resource(var id: Long = 0, var location: String = "")
data class Book(var isbn: String) : Resource()

However, this fails because of a clash between generated component1() methods. If I remove the data modifier from one of the classes, that still does not fully solve the design problem.

I am looking for the Kotlin idiom here: is there a proper way to extend a data class?

One possible workaround is to make the parent class open and override all inherited properties in the child data class:

open class Resource(open var id: Long = 0, open var location: String = "")

data class Book(
    override var id: Long = 0,
    override var location: String = "",
    var isbn: String
) : Resource()

But this feels repetitive and awkward. Is there a better approach?

Short Answer

By the end of this page, you will understand why Kotlin data class types do not work well with inheritance, why generated methods like componentN() cause conflicts, and which alternatives are typically used instead. You will also see practical patterns such as composition, interfaces, and using a regular base class with a data-class child only when it truly fits.

Concept

Kotlin data class is designed for holding data, not for participating in deep inheritance hierarchies.

When you mark a class as data, Kotlin automatically generates useful methods based on the properties declared in the primary constructor, including:

  • equals()
  • hashCode()
  • toString()
  • copy()
  • componentN() functions for destructuring

These generated methods are tightly tied to the exact constructor properties of that class. Because of that, inheritance becomes problematic.

Why extending a data class is not supported well

A data class in Kotlin cannot be open, so this is invalid:

open data class Resource(var id: Long = 0, var location: String = "")

Kotlin intentionally prevents this because inheritance would make the generated methods ambiguous or misleading.

For example:

  • Should Book.equals() compare only isbn?
  • Or isbn, id, and location?
  • How should copy() behave across parent and child classes?
  • Which class owns component1()?

These questions do not have a clean, universal answer, so Kotlin keeps data class semantics simple.

Why the component1() clash happens

Destructuring in Kotlin uses componentN() functions.

If both parent and child generate their own component1(), component2(), and so on, the compiler cannot resolve those inherited/generated members cleanly.

For example:

  • Resource might generate:
    • component1() for id
    • component2() for location
  • Book might generate:
    • component1() for isbn

That creates a naming collision and breaks destructuring semantics.

The Kotlin idiom

Instead of extending one data class from another, Kotlin code usually uses one of these approaches:

  1. Composition: put one object inside another
  2. Interfaces: define a shared contract without state inheritance
  3. Regular base class + data-class child: possible in some cases, but often repetitive
  4. Separate data classes: keep each model independent

In practice, composition is usually the cleanest alternative.

Example:

data class Resource(val id: Long = 0, val location: String = "")

data class Book(
    val resource: Resource,
    val isbn: String
)

This avoids inheritance problems while keeping the full benefit of data classes.

Mental Model

Think of a Kotlin data class like a prebuilt identity card generator.

When you create a data class, Kotlin automatically prints a card with:

  • how to compare the object
  • how to copy it
  • how to list its fields
  • how to destructure it

That generator expects a single, clear list of fields from one class.

Inheritance is like trying to combine two identity card generators from a parent and a child. Now the system no longer knows:

  • which fields define identity
  • which order the fields should appear in
  • which generated methods should win

So Kotlin avoids this confusion by keeping data classes simple and final.

A good rule of thumb is:

  • Use data classes for plain value objects
  • Use inheritance for shared behavior only when you really need polymorphism
  • Use composition when one object naturally contains another
Take Quiz

Syntax and Examples

Core rule

A Kotlin data class:

  • cannot be open
  • cannot extend another data class
  • may extend a regular class or implement an interface, but this must be designed carefully

Invalid example

open data class Resource(var id: Long = 0, var location: String = "")

This is invalid because data class cannot be open.

Better option 1: composition

data class Resource(
    val id: Long = 0,
    val location: String = ""
)

data class Book(
    val resource: Resource,
    val isbn: String
)

Why this works

  • Resource is still a reusable data holder
  • Book keeps all data-class features
  • no method clashes occur
  • the design is explicit

Usage:

val book = Book(
    resource = Resource(id = 1, location = "Shelf A"),
    isbn = "978-1234567890"
)

println(book)
println(book.resource.location)

Better option 2: interface for shared properties

interface HasResourceInfo {
    val id: Long
    val location: String
}

data class Resource(
    override val id: Long = 0,
    override val location: String = ""
) : HasResourceInfo

data class Book(
    override val id: Long = 0,
    override val location: String = "",
    val isbn: String
) : HasResourceInfo

Why this works

  • both classes share a common contract
  • no inherited state or generated-method collision
  • useful when multiple models expose the same fields

Possible but repetitive option: regular base class

open class Resource(
    open val id: Long = 0,
    open val location: String = ""
)

data class Book(
    override val id: Long = 0,
    override val location: String = "",
    val isbn: String
) : Resource(id, location)

This compiles, but it is often verbose because the child must repeat inherited properties in its constructor if you want them included in generated data class methods.

Step by Step Execution

Consider this composition-based example:

data class Resource(val id: Long, val location: String)

data class Book(val resource: Resource, val isbn: String)

val book = Book(Resource(10, "Room 2"), "978-1111111111")
println(book)

Step by step

1. Resource is defined

data class Resource(val id: Long, val location: String)

Kotlin generates:

  • equals()
  • hashCode()
  • toString()
  • copy()
  • component1() for id
  • component2() for location

2. Book is defined

data class Book(val resource: Resource, val isbn: String)

Kotlin generates separate methods for Book:

  • component1() for resource
  • component2() for isbn

There is no conflict because Book does not inherit another data class.

3. A Book object is created

val book = Book(Resource(10, "Room 2"), "978-1111111111")

This creates:

  • a Resource object with id = 10 and location = "Room 2"
  • a Book object that contains that Resource

4. println(book) is executed

Because Book is a data class, Kotlin uses its generated toString():

Book(resource=Resource(id=10, location=Room 2), isbn=978-1111111111)

Destructuring example

val (resource, isbn) = book
println(resource.id)
println(isbn)

Here:

  • resource gets book.component1()
  • isbn gets book.component2()

Again, no ambiguity exists.

Real World Use Cases

API response models

When working with API data, you often receive nested objects instead of inheritance-based models.

data class UserMeta(val id: Long, val location: String)
data class User(val meta: UserMeta, val name: String)

This mirrors JSON structures cleanly.

Database or persistence models

A base record such as id and timestamps is often shared across entities. Instead of inheriting data classes, teams frequently:

  • use a regular base class for framework needs, or
  • keep common fields duplicated in separate data classes, or
  • wrap shared fields in a nested value object

Domain modeling

If Book has a resource identity, composition reads naturally:

  • a book has resource info
  • an image has resource info
  • a video has resource info

That is often clearer than forcing an inheritance tree.

UI state objects

In Android or desktop apps, data classes are commonly used for immutable view state.

data class LoadingState(val message: String)
data class ContentState(val items: List<String>)

These are usually kept flat and simple rather than inherited from one another.

Take Quiz

Real Codebase Usage

In real Kotlin projects, developers usually avoid data-class inheritance and prefer a few practical patterns.

1. Composition for shared fields

A common pattern is to group reusable fields into another class.

data class AuditInfo(val createdBy: String, val updatedBy: String)

data class Article(
    val title: String,
    val auditInfo: AuditInfo
)

This keeps models modular and readable.

2. Interfaces for shared contracts

If multiple types must expose the same properties, an interface is often enough.

interface Identifiable {
    val id: Long
}

Then any data class can implement it without inheriting generated data-class behavior.

3. Regular base classes for framework integration

Sometimes frameworks require a superclass. In that case, teams use:

  • a normal open class for shared behavior
  • data classes only at the leaf level, if useful

But they do this carefully because equality and copying semantics can become less obvious.

4. Prefer val for value models

Many codebases use immutable data classes:

data class Book(val id: Long, val location: String, val isbn: String)

This is often simpler than modeling inheritance.

5. Guarding against overusing inheritance

Experienced Kotlin developers often ask:

  • Do these classes really share behavior?
  • Or do they only share a few fields?

If the answer is “they only share fields,” composition is usually the better design.

Common Mistakes

Mistake 1: Trying to make a data class open

open data class Resource(val id: Long)

This is not allowed.

Avoid it by

  • using a regular open class, or
  • using composition instead

Mistake 2: Expecting inherited properties to be part of child data-class generation automatically

open class Resource(open val id: Long)
data class Book(val isbn: String) : Resource(1)

Here, Book's generated methods only consider properties in Book's primary constructor.

So id is not part of Book's equals(), copy(), or destructuring unless you redeclare it in the child constructor.

Avoid it by

  • explicitly placing all data-class fields in the child constructor, or
  • choosing composition

Mistake 3: Using inheritance when the relationship is really “has-a”

If a book contains resource information, this is often better:

data class Resource(val id: Long, val location: String)
data class Book(val resource: Resource, val isbn: String)

Instead of forcing:

open class Resource(open val id: Long, open val location: String)
data class Book(
    override val id: Long,
    override val location: String,
    val isbn: String
) : Resource(id, location)

Mistake 4: Confusing shared fields with shared behavior

Inheritance is best when subclasses share behavior and can be used polymorphically.

If you only want to reuse fields, inheritance may make the design harder to understand.

Mistake 5: Expecting copy() to behave like deep inheritance-aware cloning

copy() only works for the properties in that data class's primary constructor.

If you need more complex cloning across a hierarchy, data classes are usually not the right abstraction.

Comparisons

ApproachBest forProsCons
Data class + compositionReusing shared dataClear, safe, idiomatic, full data-class supportRequires nested access like book.resource.id
Data class + interfaceShared contractFlexible, no state collision, good for polymorphismDoes not reuse implementation or storage
Data class child of regular classFrameworks or limited inheritance needsPossible in some casesRepetitive, inherited fields may need redeclaration
Data class extending data classNot supportedNoneConflicts with generated methods and semantics

Inheritance vs composition

StyleMeaningExampleUsually better when
Inheritance“is-a”Dog : Animalsubtype behavior matters
Composition“has-a”Book(resourceInfo)object contains reusable data

Regular class vs data class

FeatureRegular classData class
Auto toString()NoYes
Auto equals() / hashCode()NoYes
Auto copy()NoYes
Good for inheritance hierarchiesYesUsually no
Good for plain value objectsCan beYes
Take Quiz

Cheat Sheet

Quick rules

  • data class cannot be open
  • one data class should not inherit from another data class
  • generated methods are based only on properties in the primary constructor
  • inherited properties are not automatically included in child data-class features unless redeclared in the child constructor

Common alternatives

Composition

data class Resource(val id: Long, val location: String)
data class Book(val resource: Resource, val isbn: String)

Interface

interface HasId { val id: Long }

Regular base class

open class Resource(open val id: Long)
data class Book(override val id: Long, val isbn: String) : Resource(id)

Remember

  • equals(), hashCode(), copy(), and componentN() come from the primary constructor
  • if you need polymorphic shared behavior, use inheritance carefully
  • if you need shared data, prefer composition
  • if you need a shared shape, consider an interface

Red flag

If you are repeating parent properties in the child constructor just to make the data class work, that often means composition would be cleaner.

FAQ

Can a data class inherit from another data class in Kotlin?

No. Kotlin does not support data-class inheritance because generated methods like copy() and componentN() would create ambiguous behavior.

Why can't a data class be open in Kotlin?

Because data classes generate methods based on a fixed set of constructor properties. Allowing subclasses would make equality, copying, and destructuring semantics unclear.

Can a data class extend a regular class?

Yes, it can extend a non-data class if the design allows it. But only the properties declared in the data class's own primary constructor are used for generated methods.

Are inherited properties included in a child data class's equals and copy?

Not automatically. Only properties in the child data class primary constructor are included.

What is the best alternative to data-class inheritance in Kotlin?

Usually composition. If one object contains another object's data, composition is cleaner and avoids generated-method conflicts.

When should I use an interface instead of inheritance?

Use an interface when multiple classes should expose the same properties or behavior contract, but you do not need shared stored state.

Is repeating parent properties in a child data class a good idea?

It works, but it is often verbose and a sign that the model may be better represented with composition.

Take Quiz

Mini Project

Description

Create a small library catalog model where books need shared resource information such as id and location, but without using data-class inheritance. This demonstrates the Kotlin-idiomatic approach of composition.

Goal

Build a Book model that reuses resource data cleanly and supports printing, copying, and destructuring without inheritance issues.

Requirements

  • Create a Resource data class with id and location
  • Create a Book data class that contains a Resource and an isbn
  • Create at least two Book objects and print them
  • Use copy() to change one field in a book
  • Destructure one Book object and print its parts
Take Quiz

Keep learning

Related questions

View more