Recursive PII Masking in DataWeave: One Function for Any Depth (and the Null Trap)
Source: Dev.to

TL;DR
- A recursive
maskPIIfunction dispatches on type: Object → check fields, Array → recurse, Primitive → pass through. - Works at any nesting depth with a single call:
maskPII(payload). - Null values caused crashes; add explicit null handling before the Object case.
- No hard‑coded paths needed – the function finds PII fields at any level.
The Problem: PII at Unknown Depth
Our org‑chart API returns hierarchical data, e.g.:
{
"company": "Acme Corp",
"ceo": {
"name": "Alice Chen",
"ssn": "123-45-6789",
"email": "alice@acme.com",
"reports": [
{
"name": "Bob Martinez",
"ssn": "234-56-7890",
"email": "bob@acme.com",
"reports": [
{ "name": "Carol Nguyen", "ssn": "345-67-8901" }
]
}
]
}
}SSNs appear at multiple levels (CEO, VP, Director, etc.). The compliance requirement is to mask all SSNs, regardless of depth (some orgs go up to six levels).
The Recursive Solution
%dw 2.0
output application/json
fun maskSsn(s: String): String = "***-**-" ++ s[-4 to -1]
fun maskEmail(e: String): String =
do {
var parts = e splitBy "@"
parts[0][0] ++ "****@" ++ parts[1]
}
fun maskPII(data: Any): Any =
data match {
case obj is Object ->
obj mapObject (value, key) ->
if ((key as String) == "ssn") {(key): maskSsn(value as String)}
else if ((key as String) == "email") {(key): maskEmail(value as String)}
else {(key): maskPII(value)}
case arr is Array -> arr map maskPII($)
else -> data
}
---
maskPII(payload)Type dispatch
- Object – field‑level checking.
- Array – recurse into each element.
- Primitive – pass through unchanged.
A single call maskPII(payload) handles any depth.
100 production‑ready DataWeave patterns with tests: mulesoft‑cookbook on GitHub
The Null Trap
Production payloads sometimes contain null values (e.g., a manager without an email). The original data match block attempted to treat null as an object, causing a mapObject crash and resulting in 400 API responses.
Fix – explicit null handling
fun maskPII(data: Any): Any =
data match {
case is Null -> null
case obj is Object -> obj mapObject (value, key) ->
if ((key as String) == "ssn") {(key): maskSsn(value as String)}
else if ((key as String) == "email") {(key): maskEmail(value as String)}
else {(key): maskPII(value)}
case arr is Array -> arr map maskPII($)
else -> data
}The case is Null -> null clause catches nulls before they reach the object handler, allowing them to pass through unchanged.
Testing Recursive Functions
Edge cases exercised:
- Empty object
{}at each level - Empty array
[]at each level nullat each level- Mixed types in arrays:
["text", 42, null, {"key": "value"}] - Maximum expected depth (6 levels for our org chart)
All tests run in the DataWeave Playground, preventing recursive bugs.
Additional resources
- 100 patterns with MUnit tests: github.com/shakarbisetty/mulesoft-cookbook
- 60‑second video walkthroughs: youtube.com/@SanThaParv