Structured diffing of Kotlin data classes and maps with change tracking
implementation com.philiprehberger:diff-kitStructured diffing of Kotlin data classes and maps with change tracking.
implementation("com.philiprehberger:diff-kit:0.3.0")
<dependency>
<groupId>com.philiprehberger</groupId>
<artifactId>diff-kit</artifactId>
<version>0.3.0</version>
</dependency>
import com.philiprehberger.diffkit.*
data class User(val name: String, val age: Int, val email: String)
val old = User("Alice", 30, "alice@example.com")
val new = User("Alice", 31, "alice@new.com")
val result = diff(old, new)
println(result.hasChanges()) // true
println(result.changedPaths()) // [age, email]
println(result)
// age: 30 -> 31
// email: alice@example.com -> alice@new.com
data class Address(val street: String, val city: String)
data class Person(val name: String, val address: Address)
val result = diff(
Person("Alice", Address("Main St", "Springfield")),
Person("Alice", Address("Main St", "Shelbyville"))
)
println(result.changedPaths()) // [address.city]
val result = diff(old, new) {
exclude("email", "updatedAt")
}
Exclude fields using wildcard patterns with *:
val result = diff(old, new) {
// Exclude all "metadata" sub-fields
exclude("metadata.*")
// Exclude "updatedAt" at any depth
exclude("*.updatedAt")
}
Supply custom comparators for specific field paths:
val result = diff(old, new) {
// Case-insensitive string comparison for the "name" field
comparator("name", Comparator { a, b ->
(a as String).lowercase().compareTo((b as String).lowercase())
})
// Numeric tolerance for floating-point fields
comparator("value", Comparator { a, b ->
val diff = (a as Double) - (b as Double)
if (kotlin.math.abs(diff) < 0.01) 0 else diff.compareTo(0.0)
})
}
Lists are compared element by element, showing individual additions, removals, and changes with their indices:
data class Item(val id: Int, val value: String)
data class Container(val items: List<Item>)
val old = Container(listOf(Item(1, "a"), Item(2, "b")))
val new = Container(listOf(Item(1, "a"), Item(2, "updated"), Item(3, "c")))
val result = diff(old, new)
// items[1].value: b -> updated
// items[2]: added Item(id=3, value=c)
Sets are compared for added and removed elements:
data class TaggedItem(val name: String, val tags: Set<String>)
val old = TaggedItem("item", setOf("a", "b"))
val new = TaggedItem("item", setOf("b", "c"))
val result = diff(old, new)
// tags: removed a
// tags: added c
Get counts of changes by type:
val result = diff(old, new)
val summary = result.summary()
println(summary.added) // number of additions
println(summary.removed) // number of removals
println(summary.changed) // number of modifications
println(summary.total) // total count
You can also call summary() on any List<Change>:
val summary = result.changes.summary()
Convert a diff result into a map of just the changed values:
val patch = diff(old, new).toPatchMap()
// e.g., { "age" to 31, "email" to "alice@new.com" }
Removed fields appear with null values, added and changed fields contain the new value.
val result = diffMaps(
mapOf("a" to 1, "b" to 2, "c" to 3),
mapOf("b" to 20, "c" to 3, "d" to 4)
)
println(result.added) // {d=4}
println(result.removed) // {a=1}
println(result.changed) // {b=(2, 20)}
val result = diff(oldConfig, newConfig) {
ignorePaths("secret", "password", "internal.token")
}
val result = diff(oldUser, newUser)
result.humanReadable().forEach { println(it) }
// 'name' changed from Alice to Bob
// 'age' changed from 30 to 31
val original = mapOf("name" to "Alice", "age" to 30)
val patched = applyPatch(original, diffResult)
| Class / Function | Description |
|---|---|
diff(old, new, config) | Compares two data class instances recursively |
diffMaps(old, new) | Compares two maps for added, removed, and changed entries |
DiffResult | Contains the list of Change objects |
DiffResult.hasChanges() | Returns true if any differences were found |
DiffResult.changedPaths() | Returns the list of changed property paths |
DiffResult.summary() | Returns a DiffSummary with add/remove/change counts |
DiffResult.toPatchMap() | Converts changes to a Map<String, Any?> of new values |
Change | A single change with path, oldValue, newValue, and type |
ChangeType | Enum: CHANGED, ADDED, REMOVED |
DiffSummary | Counts: added, removed, changed, total |
DiffConfig.exclude() | Excludes fields by name or wildcard pattern |
DiffConfig.ignorePaths() | Exclude exact paths from comparison |
DiffResult.humanReadable() | Generate human-readable change descriptions |
applyPatch(map, diff) | Apply diff changes to a map |
DiffConfig.comparator() | Registers a custom comparator for a field path |
List<Change>.summary() | Extension to produce a DiffSummary from any change list |
MapDiffResult | Contains added, removed, and changed map entries |
./gradlew test # Run tests
./gradlew check # Run all checks
./gradlew build # Build JAR
If you find this project useful: