Skip to content

Instantly share code, notes, and snippets.

@josdejong
Last active October 24, 2023 09:30
Show Gist options
  • Star 18 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save josdejong/fbb43ae33fcdd922040dac4ffc31aeaf to your computer and use it in GitHub Desktop.
Save josdejong/fbb43ae33fcdd922040dac4ffc31aeaf to your computer and use it in GitHub Desktop.
Merge two data classes in Kotlin
import kotlin.reflect.full.declaredMemberProperties
import kotlin.reflect.full.primaryConstructor
/**
* Merge two data classes
*
* The resulting data class will contain:
* - all fields of `other` which are non null
* - the fields of `this` for the fields which are null in `other`
*
* The function is immutable, the original data classes are not changed
* and a new data class instance is returned.
*
* Example usage:
*
* val a = MyDataClass(...)
* val b = MyDataClass(...)
* val c = a merge b
*/
infix inline fun <reified T : Any> T.merge(other: T): T {
val nameToProperty = T::class.declaredMemberProperties.associateBy { it.name }
val primaryConstructor = T::class.primaryConstructor!!
val args = primaryConstructor.parameters.associateWith { parameter ->
val property = nameToProperty[parameter.name]!!
(property.get(other) ?: property.get(this))
}
return primaryConstructor.callBy(args)
}
@huy-t
Copy link

huy-t commented Jul 18, 2019

I use your util with
implementation 'org.jetbrains.kotlin:kotlin-reflect:1.3.41'
But can not receive any merged field. :(

@josdejong
Copy link
Author

I just tested (also using Kotlin 1.3.41), all seems to work fine. How are you using it?

@huy-t
Copy link

huy-t commented Jul 20, 2019

Thanks for your response.
Here is my config:

  • kotlin_version = '1.3.40'
  • classpath 'com.android.tools.build:gradle:3.4.1'
  • implementation 'org.jetbrains.kotlin:kotlin-reflect:1.3.41'

REPL:

import com.example.test_only.ProductModel
import com.example.test_only.merge

var apple1 = ProductModel("RED", "APPLE", "SMALL")
var apple2 = ProductModel("GREEN", "APPLE", "BIG")
var apple3 = apple1.merge(apple2)
println("$apple1")
println("$apple2")
println("$apple3")
println("-----")
apple1 = ProductModel("RED", "APPLE", "SMALL")
apple2 = ProductModel("GREEN", "APPLE", "BIG")
apple3 = apple2.merge(apple1)
println("$apple1")
println("$apple2")
println("$apple3")

Output

ProductModel(color=RED, fruit=APPLE, size=SMALL)
ProductModel(color=GREEN, fruit=APPLE, size=BIG)
ProductModel(color=GREEN, fruit=APPLE, size=BIG)
-----
ProductModel(color=RED, fruit=APPLE, size=SMALL)
ProductModel(color=GREEN, fruit=APPLE, size=BIG)
ProductModel(color=RED, fruit=APPLE, size=SMALL)

@josdejong
Copy link
Author

And what does ProductModel look like?

@huy-t
Copy link

huy-t commented Jul 21, 2019

I have tried 2 model class:
package com.example.test_only

data class ProductModel(
var color: String?,
var fruit: String?,
var size: String?
)

and

app: build.gradle

androidExtensions {
    experimental = true
}

import android.os.Parcelable
import kotlinx.android.parcel.Parcelize

@parcelize
data class ProductModel(
var color: String?,
var fruit: String?,
var size: String?
) : Parcelable

@josdejong
Copy link
Author

Thanks, now I can reproduce your results. The output looks as expected to me: apple1 and apple2 stay untouched, and apple3 is the merge result of apple1 and apple2. What would you expect?

@josdejong
Copy link
Author

What output would you expect for apple3 in your two examples?

@huy-t
Copy link

huy-t commented Jul 21, 2019

Oh finally I got your ideal.

  • Stay untouched with apple1 and apple2.
  • Merged object is new object called apple3.

import com.example.test_only.ProductModel
import com.example.test_only.merge

var appleOld = ProductModel(null, "APPLE", null, 1)
var appleNew = ProductModel("GREEN", null, "BIG", 2)
var appleMerged = appleOld.merge(appleNew)
println("$appleOld")
println("$appleNew")
println("$appleMerged")
println("-----")

appleOld = ProductModel(null, "APPLE", null, 1)
appleNew = ProductModel("GREEN", null, "BIG", 2)
appleMerged = appleNew.merge(appleOld)
println("$appleOld")
println("$appleNew")
println("$appleMerged")

Output

ProductModel(color=null, fruit=APPLE, size=null, num=1)
ProductModel(color=GREEN, fruit=null, size=BIG, num=2)
ProductModel(color=GREEN, fruit=APPLE, size=BIG, num=2)

ProductModel(color=null, fruit=APPLE, size=null, num=1)
ProductModel(color=GREEN, fruit=null, size=BIG, num=2)
ProductModel(color=GREEN, fruit=APPLE, size=BIG, num=1)

Thanks for your response.
You save me all day.!

@josdejong
Copy link
Author

Yes indeed :)

Will update update the explanatory comments

@huy-t
Copy link

huy-t commented Jul 21, 2019

We already did it. 👍

@Pekwerike
Copy link

hoiulj

@tonsV2
Copy link

tonsV2 commented Jun 10, 2020

I'm merging objects which has inherited properties so I ended up using the following snippet.

Also .associateWith simplifies the loop a little.

import kotlin.reflect.full.memberProperties
import kotlin.reflect.full.primaryConstructor

inline infix fun <reified T : Any> T.merge(other: T): T {
    val nameToProperty = T::class.memberProperties.associateBy { it.name }
    val primaryConstructor = T::class.primaryConstructor!!
    val args = primaryConstructor.parameters.associateWith { parameter ->
        val property = nameToProperty[parameter.name]!!
        property.get(other) ?: property.get(this)
    }
    return primaryConstructor.callBy(args)
}

@e-g-hategan
Copy link

I was hoping this would work with let's call them deep classes?
data class Some(val a: String?, val b: String?)
data class Other(val some: Some?, val c: String?, val d: String?)

when i do Other(Some("a", null), "c", null).merge(Other(Some(null, "b"), null, "d))
I'd expect to get Other(Some("a", "b"), "c", "d")

@josdejong
Copy link
Author

It's indeed a shallow merge. Deep merge would be interesting.

@MurtuzaSrashtaSoft
Copy link

@josdejong did you have any solution about. A deep merge would be interesting?

@MurtuzaSrashtaSoft
Copy link

@e-g-hategan I have the same requirement.

i m Doing Single level marge using This Code

inline infix fun <reified T : Any> T.merge(other: T): T {
    val nameToProperty = T::class.declaredMemberProperties.associateBy { it.name }
    val primaryConstructor = T::class.primaryConstructor!!
    val args =
        primaryConstructor.parameters.associateWith { parameter ->
            val property = nameToProperty[parameter.name]!!
            (property.get(other) ?: property.get(this))
        }
    val mergedObject = primaryConstructor.callBy(args)
    nameToProperty.values.forEach { it ->
        run {
            val property = it as KMutableProperty<*>
            val value = property.javaGetter!!.invoke(other) ?: property.javaGetter!!.invoke(this)
            property.javaSetter!!.invoke(mergedObject, value)
        }
    }
    return mergedObject
} 

@talatkuyuk
Copy link

How can I add extra condition, for example:

  • The resulting data class will contain:
    • all fields of other which are non null or zero
    • the fields of this for the fields which are null or zero in other

or

  • The resulting data class will contain:
    • all fields of other which are non null or Double 0.0
    • the fields of this for the fields which are null or Double 0.0 in other

Which line I have to put this kind of conditions?

@josdejong
Copy link
Author

@talatkuyuk this logic captured in the line:

parameter to (property.get(other) ?: property.get(this))

Right now the behavior is: take the property from other, but if that is null, take the property from from this.

You can adjust the exact behavior of when to pick a property from either other or this as you like, i.e. add an extra conditional check to pick other only when it is non null and not zero (when it is a numeric value).

@talatkuyuk
Copy link

talatkuyuk commented May 9, 2021

@josdejong Thank you very much. I understand it, but I am new in Kotlin. I am not familiar with to operator. I didn't write any inflix function yet. Just I needed this.

  • The resulting data class will contain:
    • all fields of other which are non null or non Double with value 0.0
    • the fields of this for the fields which are null or Double with value 0.0 in other

I tried to make it, I am stuck.

@josdejong
Copy link
Author

Something like this - just change the exact logic to meet your needs (and think through what logic you need for Double, Float, Int. etc, there may be a smart way to check whether any numeric type is zero, I don't know):

    infix inline fun <reified T : Any> T.mergeIgnoreZero(other: T): T {
        val nameToProperty = T::class.declaredMemberProperties.associateBy { it.name }
        val primaryConstructor = T::class.primaryConstructor!!
        val args = primaryConstructor.parameters.associateWith { parameter ->
            val property = nameToProperty[parameter.name]!!
            val valueOther = property.get(other)
            val valueThis = property.get(this)

            if (valueOther is Double && valueOther == 0.0) {
                valueThis
            } else {
                valueOther ?: valueThis
            }
        }
        return primaryConstructor.callBy(args)
    }

@talatkuyuk
Copy link

@josdejong Thank you very much. It worked.

@josdejong
Copy link
Author

👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment