AnyMap Deep Merge with Kotlin
How to merge two JSONs with no additional libraries
Like my previous “Kotlin retry” wrapper function, this is another handy function from my collection when I need to deep merge any two JSON-like Maps without additional libraries.

Dealing with JSON could be easy for loosely typed programming languages like TypeScript or Python, but with Kotlin or Java, it's not a straightforward task. To represent JSON with Kotlin object, we must explicitly define all fields or use some special datatypes.
Practical need
I was developing REST API with frequently changed definitions, so instead of a particular input datatype, I came up with a controller like this:
@POST
@Consumes(MediaType.APPLICATION_JSON)
fun createUser(
data: Map<String, Any>
)
This definition allows receiving any JSON in the “data” field, but…
“The cost of flexibility is complexity” — Martin Fowler
When I wanted to add default values to some input parameters, I ended up merging two JSONs (input and config) represented by Map<String, Any> type.
There is no standard function in Kotlin to deep merge two nested JSON-like maps, so I wrote my own method with recursions to do that.
Method definition
First, we define a custom type of our AnyMap to store JSON representation.
typealias AnyMap = Map<String, Any>
Then we define an extension function to safely cast nested maps, omit compiler warnings and throw explicit errors if something goes wrong. Since it is two JSON-like maps, we expect MapKey to be a String.
val Any?.asAnyMap: AnyMap
get() {
val resultMap = mutableMapOf<String, Any>()
if (this is Map<*, *>) {
for (item in this) {
val key = item.key
val value = item.value
if (key != null && value != null) {
if (key is String) {
resultMap[key] = value
} else {
throw RuntimeException("Expected Map<String,Any> but was Map<${key::class.simpleName},${value::class.simpleName}>")
}
}
}
} else {
val typeName = if (this == null) "null" else this::class.simpleName
throw RuntimeException("Expected Map<*,*> but was $typeName")
}
return resultMap
}
Finally, we define a method that merges two maps, including nested maps and collections. It also respects any “null” values in the source map, so they are not overridden by merge.
private fun deepMerge(destination: AnyMap, source: AnyMap): AnyMap {
val resultMap = destination.toMutableMap()
for (key in source.keys) {
//recursive merge for nested maps
if (source[key] is Map<*, *> && resultMap[key] is Map<*, *>) {
val originalChild = resultMap[key].asAnyMap
val newChild = source[key].asAnyMap
resultMap[key] = deepMerge(originalChild, newChild)
//merge for collections
} else if (source[key] is Collection<*> && resultMap[key] is Collection<*>) {
if (!(resultMap[key] as Collection<*>).containsAll(source[key] as Collection<*>)) {
resultMap[key] = (resultMap[key] as Collection<*>) + (source[key] as Collection<*>)
}
} else {
if (source[key] == null || (source[key] is String && (source[key] as String).isBlank())) continue
resultMap[key] = source[key] as Any
}
}
return resultMap
}
You should be aware that build in “+” operator does a shallow merge by default.
If you do not expect a shallow merge and want to eliminate the confusion, you can override “+” operator to use your newly created deep merge function.
operator fun AnyMap.plus(source: AnyMap): AnyMap {
return deepMerge(this, source)
}
Usage
Let's imagine that we need to merge the two following nested maps.
val map1 = mapOf(
"name" to "John",
"address" to mapOf(
"nums" to listOf(1, 2),
"line1" to 1,
"line2" to null
)
)
val map2 = mapOf(
"age" to 18,
"address" to mapOf(
"nums" to listOf(3),
"line2" to 2,
"line3" to 3
)
)
Now we can compare the outcome of merging with shallow and deep merge functions using “+” operator.
// BEFORE - shalow merge/replace, standart behaviour
print(map1 + map2)
//{name=John, address={nums=[3], line2=2, line3=3}, age=18}
// AFTER - deep merge, overridden ’+’
println(map1 + map2)
//{name=John, address={nums=[1, 2, 3], line1=1, line2=2, line3=3}, age=18}
Conclusion
Sometimes it's required to make your REST API controller flexible enough to receive any JSON as an input object and deal with it dynamically after. While there are multiple ways of doing that, I prefer representing JSON with standard Map<String, Any>
type instead of special JSONObject
datatype.
Merging two Maps
by “+” operator will lead to a shallow merge. To do a proper deep merge, you must use additional libraries or leverage my custom implementation above ☝️. To avoid possible mistakes, you can also override “+” operator.
I hope it was helpful, and see you in the next round, curious reader.