DEV Community

Cover image for Recursive PII Masking in DataWeave: One Function for Any Depth (and the Null Trap)
ThaSha
ThaSha

Posted on

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

A compliance audit found SSN values 4 levels deep in our API responses last year. One recursive function masks everything. Then null values in production crashed 400 responses.

TL;DR

  • Recursive maskPII function dispatches on type: Object → check fields, Array → recurse, Primitive → pass through
  • Works at any nesting depth with one function call: maskPII(payload)
  • Null values crash it — add explicit null handling before the Object case
  • No hardcoded paths needed — the function finds PII fields at any level

The Problem: PII at Unknown Depth

Our org chart API returns hierarchical data:

{
  "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"}
        ]
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

SSN at level 1 (CEO), level 2 (VP), level 3 (Director). The compliance requirement: mask ALL of them. The depth varies per org — some go 6 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)
Enter fullscreen mode Exit fullscreen mode

Type dispatch: Objects get field-level checking. Arrays recurse into each element. Strings, numbers, booleans pass through unchanged.

One call — maskPII(payload) — handles any depth.


100 production-ready DataWeave patterns with tests: mulesoft-cookbook on GitHub


The Null Trap

Production payloads had null values at level 3 — a manager with no email. The data match block dispatched null to... somewhere unexpected. It tried mapObject on null. Crash. 400 API responses failed.

The fix: add explicit null handling.

fun maskPII(data: Any): Any =
    data match {
        case is Null -> null
        case obj is Object -> obj mapObject ...
        case arr is Array -> arr map maskPII($)
        else -> data
    }
Enter fullscreen mode Exit fullscreen mode

case is Null -> null catches null before it reaches the Object handler. Now null passes through unchanged.

Testing Recursive Functions

I test with these edge cases now:

  1. Empty object {} at each level
  2. Empty array [] at each level
  3. null at each level
  4. Mixed types in arrays: ["text", 42, null, {"key": "value"}]
  5. Maximum expected depth (6 levels for our org chart)

5 minutes of setup in the DataWeave Playground. Prevented every recursive bug since.


100 patterns with MUnit tests: github.com/shakarbisetty/mulesoft-cookbook

60-second video walkthroughs: youtube.com/@SanThaParv

Top comments (0)