Approach: Walk the input object recursively and build a shallow-deep copy while applying redaction when a path matches. Do not mutate the input — create new objects/arrays on the fly. Support dot-separated paths like "user.email" and handle arrays by descending into each element automatically (no special syntax required).javascript
// Node.js / JavaScript implementation
const REDACTED = '[REDACTED]';
/**
* Redact PII from obj according to redactionPaths (e.g. ['user.email','payment.card.number'])
* Returns a new object, does not mutate input.
*/
function redactPII(obj, redactionPaths) {
const pathTrees = buildPathTree(redactionPaths);
return redactNode(obj, pathTrees);
}
function buildPathTree(paths) {
// Build a trie-like tree for fast matching
const root = {};
for (const p of paths) {
const parts = p.split('.');
let node = root;
for (const part of parts) {
node[part] = node[part] || {};
node = node[part];
}
node.__redact = true;
}
return root;
}
function redactNode(node, pathTree) {
// primitive or null -> return as-is
if (node === null || typeof node !== 'object') return node;
if (Array.isArray(node)) {
// map each element, applying same pathTree
return node.map(el => redactNode(el, pathTree));
}
// object: copy keys; for each key check pathTree
const out = {};
for (const [k, v] of Object.entries(node)) {
if (pathTree && pathTree[k]) {
if (pathTree[k].__redact) {
// redact the whole value at this path
out[k] = REDACTED;
} else {
// descend with the subtree for this key
out[k] = redactNode(v, pathTree[k]);
}
} else {
// key not in any redaction path: still need to deep-copy to avoid mutation
out[k] = redactNode(v, null);
}
}
return out;
}
Example:Input:{ user: { name:'Alice', email:'a@x.com' }, sessions: [{ id:1, user:{ email:'a@x.com' } }], payment: { card: { number: '4111', cvv: '123' } }}Redaction paths: ['user.email', 'payment.card.number']Output:{ user: { name:'Alice', email:'[REDACTED]' }, sessions: [{ id:1, user:{ email:'[REDACTED]' } }], payment: { card: { number:'[REDACTED]', cvv:'123' } }}Key points:- Preserves structure and types, doesn't mutate input.- Supports arrays by mapping into elements automatically.- Uses a path-trie for efficient matching across multiple paths.Complexity:- Time: O(N + M * L) roughly — N = number of nodes (object properties + array elements); M = number of redaction paths, L = average path length for building the trie. Matching cost per node is O(1) lookup into current trie level.- Space: O(N) for returned copy + O(M * L) for path-trie.Edge cases:- Non-existent paths are ignored.- Nulls and primitives handled safely.- If you need wildcard paths (e.g., redact any key named "email" anywhere), extend buildPathTree to support special token '*' and check it during traversal.