From db67a357f43ebf0568355862c79d50bb3f070596 Mon Sep 17 00:00:00 2001 From: Ruben Grossmann Date: Wed, 25 Mar 2026 15:44:52 +0100 Subject: [PATCH] Add support for wildcard `keysToSkip` in `jsonDiff` --- README.md | 4 ++++ src/jsonDiff.ts | 6 ++++++ tests/jsonDiff.test.ts | 47 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 57 insertions(+) diff --git a/README.md b/README.md index 640d9e5..d292322 100644 --- a/README.md +++ b/README.md @@ -614,7 +614,11 @@ diff(old, new, { arrayIdentityKeys: { tags: '$value' } }); #### Path Skipping ```typescript +// Skip an exact path and all its children diff(old, new, { keysToSkip: ['characters.metadata'] }); + +// Skip all children of a path, but still detect ADD/REMOVE of the node itself +diff(old, new, { keysToSkip: ['characters.metadata.*'] }); ``` #### Type Change Handling diff --git a/src/jsonDiff.ts b/src/jsonDiff.ts index 58f0b01..41ffa97 100644 --- a/src/jsonDiff.ts +++ b/src/jsonDiff.ts @@ -380,6 +380,12 @@ const compare = (oldObj: any, newObj: any, path: any, keyPath: any, options: Opt return true; } + // The current path is inside an excluded parent by wildcard + if (skipPath.endsWith('.*')) { + const basePath = skipPath.slice(0, -2); + return currentPath.startsWith(basePath + '.'); + } + // The current path is a parent of the skip path if (skipPath.includes('.') && skipPath.startsWith(currentPath + '.')) { return false; // Don't skip, we need to process the parent diff --git a/tests/jsonDiff.test.ts b/tests/jsonDiff.test.ts index f3ccfe5..47e79b9 100644 --- a/tests/jsonDiff.test.ts +++ b/tests/jsonDiff.test.ts @@ -133,6 +133,53 @@ describe('jsonDiff#diff', () => { ]); }); + describe('wildcard keysToSkip (property.*)', () => { + const base = { + property: { + name: 'Alice', + address: { + formattedAddress: '123 Main St', + utcOffset: 0, + } + } + }; + + it('ignores property changes inside the wildcarded key', () => { + const withChangedAddress = { + property: { + name: 'Alice', + address: { + formattedAddress: 'New Address', + utcOffset: 5, + } + } + }; + expect(diff(base, withChangedAddress, { keysToSkip: ['property.address.*'] })).toEqual([]); + }); + + it('detects removal of the wildcarded key itself', () => { + const withoutAddress = { property: { name: 'Alice' } }; + expect(diff(base, withoutAddress, { keysToSkip: ['property.address.*'] })).toEqual([ + { + type: 'UPDATE', + key: 'property', + changes: [{ type: 'REMOVE', key: 'address', value: base.property.address }] + } + ]); + }); + + it('detects addition of the wildcarded key itself', () => { + const withoutAddress = { property: { name: 'Alice' } }; + expect(diff(withoutAddress, base, { keysToSkip: ['property.address.*'] })).toEqual([ + { + type: 'UPDATE', + key: 'property', + changes: [{ type: 'ADD', key: 'address', value: base.property.address }] + } + ]); + }); + }); + it.each(fixtures.assortedDiffs)( 'correctly diffs $oldVal with $newVal', ({ oldVal, newVal, expectedReplacement, expectedUpdate }) => {