Mask values inside a nested JSON-like map by dot-notation paths (user.email, profile.address.zip). Non-existent paths are silently skipped; non-string leaves are left alone; the original map must not be mutated — return a fresh structure.
This is the masker for JSONB / JSON column types in Synthesized.io — masking happens before serialization back to disk.
Tests this task must pass
Simple nested path — user.email is masked; sibling fields (user.name, user.age) are untouched.
Non-existent path is a no-op — user.phone when there is no phone returns the input unchanged (deep equality preserved).
Multiple paths in one call — user.email and user.name can be masked together; non-listed siblings stay intact.
Top-level path — notes at the root level is masked.
No mutation — the original input map (and its nested maps) is unchanged after the call.
//sampleStart
fun maskJsonPaths(json: Map<String, Any?>, paths: List<String>, masker: (String) -> String): Map<String, Any?> {
// Mask values at specified dot-notation paths in a nested map.
// "user.email" → navigate to json["user"]["email"], apply masker
// Non-existent paths → ignore, return json unchanged
// Only mask String values. Leave numbers/booleans untouched.
// Return a NEW map (don't mutate input).
TODO()
}
//sampleEnd
fun main() {
val masker: (String) -> String = { "***" }
val json = mapOf(
"user" to mapOf("email" to "alex@test.com", "age" to 30, "name" to "Alex"),
"notes" to "some notes",
"count" to 42
)
// Test 1: Simple path
val r1 = maskJsonPaths(json, listOf("user.email"), masker)
val user1 = r1["user"] as Map<*, *>
check(user1["email"] == "***") { "FAIL: email should be masked. Got: ${user1["email"]}" }
check(user1["name"] == "Alex") { "FAIL: name untouched" }
check(user1["age"] == 30) { "FAIL: age untouched" }
println("✅ Test 1: user.email masked")
// Test 2: Non-existent path — no error
val r2 = maskJsonPaths(json, listOf("user.phone"), masker)
check(r2 == json) { "FAIL: non-existent path should return unchanged" }
println("✅ Test 2: Non-existent path ignored")
// Test 3: Multiple paths
val r3 = maskJsonPaths(json, listOf("user.email", "user.name"), masker)
val user3 = r3["user"] as Map<*, *>
check(user3["email"] == "***" && user3["name"] == "***") { "FAIL: both masked" }
check(user3["age"] == 30) { "FAIL: age untouched" }
println("✅ Test 3: Multiple paths masked")
// Test 4: Top-level path
val r4 = maskJsonPaths(json, listOf("notes"), masker)
check(r4["notes"] == "***") { "FAIL: top-level masked" }
check(r4["count"] == 42) { "FAIL: count untouched" }
println("✅ Test 4: Top-level path")
// Test 5: Don't mutate original
check((json["user"] as Map<*,*>)["email"] == "alex@test.com") { "FAIL: original mutated!" }
println("✅ Test 5: Original not mutated")
println("\n🎉 ALL TESTS PASSED!")
}
Hint
Recurse on the path: at each level, copy the current map (toMutableMap()) and re-set the key with either the masked value (last segment) or a recursively-rebuilt sub-map (more segments). If the key is missing, return the input unchanged — propagating that "no change" through the whole call chain keeps the equality check in test 2 passing.
Solution
@Suppress("UNCHECKED_CAST")
fun maskJsonPaths(
json: Map<String, Any?>,
paths: List<String>,
masker: (String) -> String
): Map<String, Any?> {
fun apply(map: Map<String, Any?>, parts: List<String>): Map<String, Any?> {
if (parts.isEmpty()) return map
val key = parts.first()
if (key !in map) return map
val rest = parts.drop(1)
val current = map[key]
val newValue: Any? = when {
rest.isEmpty() -> if (current is String) masker(current) else current
current is Map<*, *> -> apply(current as Map<String, Any?>, rest)
else -> current
}
if (newValue === current) return map
return map.toMutableMap().also { it[key] = newValue }
}
var result = json
for (path in paths) result = apply(result, path.split("."))
return result
}