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

Published: (April 5, 2026 at 12:20 PM EDT)
3 min read
Source: Dev.to

Source: Dev.to

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

TL;DR

  • A recursive maskPII function 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
  • null at 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

0 views
Back to Blog

Related posts

Read more »

DECIMAL NUMBERS

Decimal – Simple Definition A decimal number is a number written using the base‑10 system. To break a decimal number e.g., 2345 into its digits: - no % 10 → ge...