Skip to content

Commit fabe6c2

Browse files
Merge pull request #31 from gravity9-tech/feature/30_json_diff_ignore_fields
#30 Ignore fields in JsonDiff
2 parents 802bdfc + 9f0585f commit fabe6c2

File tree

4 files changed

+352
-9
lines changed

4 files changed

+352
-9
lines changed

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,17 @@ final JsonPatch patch = JsonDiff.asJsonPatch(source, target);
132132
final JsonNode patchNode = JsonDiff.asJson(source, target);
133133
```
134134

135+
It's possible to ignore fields in Json Diff. List of ignored fields should be specified as JsonPointer
136+
or JsonPath paths. If ignored field does not exist in target or source object, it's ignored.
137+
138+
```java
139+
final List<String> fieldsToIgnore = new ArrayList<>();
140+
fieldsToIgnore.add("/id");
141+
fieldsToIgnore.add("$.cars[-1:]");
142+
final JsonPatch patch = JsonDiff.asJsonPatch(source, target, fieldsToIgnore);
143+
final JsonNode patchNode = JsonDiff.asJson(source, target, fieldsToIgnore);
144+
```
145+
135146
**Important note**: the API offers **no guarantee at all** about patch "reuse";
136147
that is, the generated patch is only guaranteed to safely transform the given
137148
source to the given target. Do not expect it to give the result you expect on

src/main/java/com/gravity9/jsonpatch/diff/JsonDiff.java

Lines changed: 95 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,16 +30,23 @@
3030
import com.github.fge.msgsimple.bundle.MessageBundle;
3131
import com.github.fge.msgsimple.load.MessageBundles;
3232
import com.gravity9.jsonpatch.JsonPatch;
33+
import com.gravity9.jsonpatch.JsonPatchException;
3334
import com.gravity9.jsonpatch.JsonPatchMessages;
35+
import com.gravity9.jsonpatch.JsonPatchOperation;
36+
import com.gravity9.jsonpatch.RemoveOperation;
37+
import com.jayway.jsonpath.PathNotFoundException;
38+
39+
import javax.annotation.ParametersAreNonnullByDefault;
3440
import java.io.IOException;
41+
import java.util.ArrayList;
3542
import java.util.Collections;
3643
import java.util.HashMap;
3744
import java.util.HashSet;
3845
import java.util.Iterator;
46+
import java.util.List;
3947
import java.util.Map;
4048
import java.util.Set;
4149
import java.util.TreeSet;
42-
import javax.annotation.ParametersAreNonnullByDefault;
4350

4451
/**
4552
* JSON "diff" implementation
@@ -96,6 +103,57 @@ public static JsonPatch asJsonPatch(final JsonNode source,
96103
return processor.getPatch();
97104
}
98105

106+
/**
107+
* Generate a JSON patch for transforming the source node into the target
108+
* node ignoring given fields
109+
*
110+
* @param source the node to be patched
111+
* @param target the expected result after applying the patch
112+
* @param fieldsToIgnore list of JsonPath or JsonPointer paths which should be ignored when generating diff. Non-existing fields are ignored.
113+
* @return the patch as a {@link JsonPatch}
114+
* @throws JsonPatchException if fieldsToIgnored not in valid JsonPath or JsonPointer format
115+
* @since 2.0.0
116+
*/
117+
public static JsonPatch asJsonPatchIgnoringFields(final JsonNode source,
118+
final JsonNode target, final List<String> fieldsToIgnore) throws JsonPatchException {
119+
BUNDLE.checkNotNull(source, "common.nullArgument");
120+
BUNDLE.checkNotNull(target, "common.nullArgument");
121+
final List<JsonPatchOperation> ignoredFieldsRemoveOperations = getJsonPatchRemoveOperationsForIgnoredFields(fieldsToIgnore);
122+
123+
JsonNode sourceWithoutIgnoredFields = removeIgnoredFields(source, ignoredFieldsRemoveOperations);
124+
JsonNode targetWithoutIgnoredFields = removeIgnoredFields(target, ignoredFieldsRemoveOperations);
125+
126+
final Map<JsonPointer, JsonNode> unchanged
127+
= getUnchangedValues(sourceWithoutIgnoredFields, targetWithoutIgnoredFields);
128+
final DiffProcessor processor = new DiffProcessor(unchanged);
129+
130+
generateDiffs(processor, JsonPointer.empty(), sourceWithoutIgnoredFields, targetWithoutIgnoredFields);
131+
return processor.getPatch();
132+
}
133+
134+
private static JsonNode removeIgnoredFields(JsonNode node, List<JsonPatchOperation> ignoredFieldsRemoveOperations) throws JsonPatchException {
135+
JsonNode nodeWithoutIgnoredFields = node;
136+
for (JsonPatchOperation operation : ignoredFieldsRemoveOperations) {
137+
nodeWithoutIgnoredFields = removeIgnoredFieldOrIgnore(nodeWithoutIgnoredFields, operation);
138+
}
139+
return nodeWithoutIgnoredFields;
140+
}
141+
142+
private static JsonNode removeIgnoredFieldOrIgnore(JsonNode nodeWithoutIgnoredFields, JsonPatchOperation operation) throws JsonPatchException {
143+
try {
144+
List<JsonPatchOperation> operationsList = new ArrayList<>();
145+
operationsList.add(operation);
146+
return new JsonPatch(operationsList).apply(nodeWithoutIgnoredFields);
147+
} catch (JsonPatchException e) {
148+
// If remove for specific path throws PathNotFound, it means that node does not contain specific field which should be ignored.
149+
// See more `empty patch if object does not contain ignored field` in diff.json file.
150+
if (e.getCause() instanceof PathNotFoundException) {
151+
return nodeWithoutIgnoredFields;
152+
}
153+
throw e;
154+
}
155+
}
156+
99157
/**
100158
* Generate a JSON patch for transforming the source node into the target
101159
* node
@@ -114,6 +172,27 @@ public static JsonNode asJson(final JsonNode source, final JsonNode target) {
114172
}
115173
}
116174

175+
/**
176+
* Generate a JSON patch for transforming the source node into the target
177+
* node ignoring given fields
178+
*
179+
* @param source the node to be patched
180+
* @param target the expected result after applying the patch
181+
* @param fieldsToIgnore list of JsonPath or JsonPointer paths which should be ignored when generating diff. Non-existing fields are ignored.
182+
* @return the patch as a {@link JsonNode}
183+
* @throws JsonPatchException if fieldsToIgnored not in valid JsonPath or JsonPointer format
184+
* @since 2.0.0
185+
*/
186+
public static JsonNode asJsonIgnoringFields(final JsonNode source, final JsonNode target, List<String> fieldsToIgnore) throws JsonPatchException {
187+
final String s;
188+
try {
189+
s = MAPPER.writeValueAsString(asJsonPatchIgnoringFields(source, target, fieldsToIgnore));
190+
return MAPPER.readTree(s);
191+
} catch (IOException e) {
192+
throw new RuntimeException("cannot generate JSON diff", e);
193+
}
194+
}
195+
117196
private static void generateDiffs(final DiffProcessor processor,
118197
final JsonPointer pointer, final JsonNode source, final JsonNode target) {
119198
if (EQUIVALENCE.equivalent(source, target))
@@ -157,24 +236,24 @@ private static void generateObjectDiffs(final DiffProcessor processor,
157236
final JsonPointer pointer, final ObjectNode source,
158237
final ObjectNode target) {
159238
final Set<String> firstFields
160-
= collect(source.fieldNames(), new TreeSet<String>());
239+
= collect(source.fieldNames(), new TreeSet<>());
161240
final Set<String> secondFields
162-
= collect(target.fieldNames(), new TreeSet<String>());
241+
= collect(target.fieldNames(), new TreeSet<>());
163242

164-
final Set<String> copy1 = new HashSet<String>(firstFields);
243+
final Set<String> copy1 = new HashSet<>(firstFields);
165244
copy1.removeAll(secondFields);
166245

167246
for (final String field : Collections.unmodifiableSet(copy1))
168247
processor.valueRemoved(pointer.append(field), source.get(field));
169248

170-
final Set<String> copy2 = new HashSet<String>(secondFields);
249+
final Set<String> copy2 = new HashSet<>(secondFields);
171250
copy2.removeAll(firstFields);
172251

173252

174253
for (final String field : Collections.unmodifiableSet(copy2))
175254
processor.valueAdded(pointer.append(field), target.get(field));
176255

177-
final Set<String> intersection = new HashSet<String>(firstFields);
256+
final Set<String> intersection = new HashSet<>(firstFields);
178257
intersection.retainAll(secondFields);
179258

180259
for (final String field : intersection)
@@ -222,7 +301,7 @@ private static void generateArrayDiffs(final DiffProcessor processor,
222301

223302
static Map<JsonPointer, JsonNode> getUnchangedValues(final JsonNode source,
224303
final JsonNode target) {
225-
final Map<JsonPointer, JsonNode> ret = new HashMap<JsonPointer, JsonNode>();
304+
final Map<JsonPointer, JsonNode> ret = new HashMap<>();
226305
computeUnchanged(ret, JsonPointer.empty(), source, target);
227306
return ret;
228307
}
@@ -278,4 +357,13 @@ private static void computeArray(final Map<JsonPointer, JsonNode> ret,
278357
computeUnchanged(ret, pointer.append(i), source.get(i),
279358
target.get(i));
280359
}
360+
361+
private static List<JsonPatchOperation> getJsonPatchRemoveOperationsForIgnoredFields(List<String> fieldsToIgnore) {
362+
final List<JsonPatchOperation> ignoredFieldsRemoveOperations = new ArrayList<>();
363+
for (String fieldToIgnore : fieldsToIgnore) {
364+
ignoredFieldsRemoveOperations.add(new RemoveOperation(fieldToIgnore));
365+
}
366+
return ignoredFieldsRemoveOperations;
367+
}
368+
281369
}

src/test/java/com/gravity9/jsonpatch/diff/JsonDiffTest.java

Lines changed: 77 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,19 +19,23 @@
1919

2020
package com.gravity9.jsonpatch.diff;
2121

22+
import com.fasterxml.jackson.core.JsonProcessingException;
2223
import com.fasterxml.jackson.databind.JsonNode;
24+
import com.fasterxml.jackson.databind.ObjectMapper;
2325
import com.github.fge.jackson.JsonLoader;
2426
import com.github.fge.jackson.JsonNumEquals;
2527
import com.google.common.collect.Lists;
2628
import com.gravity9.jsonpatch.JsonPatch;
2729
import com.gravity9.jsonpatch.JsonPatchException;
2830
import java.io.IOException;
31+
import java.util.ArrayList;
2932
import java.util.Iterator;
3033
import java.util.List;
3134
import org.testng.annotations.DataProvider;
3235
import org.testng.annotations.Test;
3336

3437
import static org.assertj.core.api.Assertions.assertThat;
38+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
3539

3640
public final class JsonDiffTest {
3741

@@ -50,7 +54,9 @@ public Iterator<Object[]> getPatchesOnly() {
5054
final List<Object[]> list = Lists.newArrayList();
5155

5256
for (final JsonNode node : testData)
53-
list.add(new Object[]{node.get("first"), node.get("second")});
57+
if (!node.has("ignoreFields")) {
58+
list.add(new Object[]{node.get("first"), node.get("second")});
59+
}
5460

5561
return list.iterator();
5662
}
@@ -73,7 +79,7 @@ public Iterator<Object[]> getLiteralPatches() {
7379
final List<Object[]> list = Lists.newArrayList();
7480

7581
for (final JsonNode node : testData) {
76-
if (!node.has("patch"))
82+
if (!node.has("patch") || node.has("ignoreFields"))
7783
continue;
7884
list.add(new Object[]{
7985
node.get("message").textValue(), node.get("first"),
@@ -97,4 +103,73 @@ public void generatedPatchesAreWhatIsExpected(final String message,
97103
+ "expected: %s\nactual: %s\n", message, expected, actual
98104
).isTrue();
99105
}
106+
107+
@DataProvider
108+
public Iterator<Object[]> getDiffsWithIgnoredFields() {
109+
final List<Object[]> list = Lists.newArrayList();
110+
111+
for (final JsonNode node : testData) {
112+
if (node.has("ignoreFields")) {
113+
list.add(new Object[]{
114+
node.get("message").textValue(), node.get("first"),
115+
node.get("second"), node.get("patch"), node.get("ignoreFields")
116+
});
117+
}
118+
}
119+
120+
return list.iterator();
121+
}
122+
123+
@Test(
124+
dataProvider = "getDiffsWithIgnoredFields"
125+
)
126+
public void generatedPatchesIgnoreFields(final String message,
127+
final JsonNode first, final JsonNode second, final JsonNode expected,
128+
final JsonNode ignoreFields) throws JsonPatchException {
129+
130+
final List<String> ignoreFieldsList = new ArrayList<>();
131+
final Iterator<JsonNode> ignoreFieldsIterator = ignoreFields.elements();
132+
while (ignoreFieldsIterator.hasNext()) {
133+
ignoreFieldsList.add(ignoreFieldsIterator.next().textValue());
134+
}
135+
136+
final JsonNode actual = JsonDiff.asJsonIgnoringFields(first, second, ignoreFieldsList);
137+
138+
assertThat(EQUIVALENCE.equivalent(expected, actual)).overridingErrorMessage(
139+
"patch is not what was expected\nscenario: %s\n"
140+
+ "expected: %s\nactual: %s\n", message, expected, actual
141+
).isTrue();
142+
}
143+
144+
@DataProvider
145+
public Iterator<Object[]> getInvalidIgnoreFieldsExpressions() {
146+
final List<Object[]> list = Lists.newArrayList();
147+
list.add(new Object[]{
148+
"$.a[(@.length-1)]", "Could not parse token starting at position 3. Expected ?, ', 0-9, * "
149+
});
150+
list.add(new Object[]{
151+
"/a/?", "Invalid path, `?` are not allowed in JsonPointer expressions."
152+
});
153+
return list.iterator();
154+
}
155+
156+
@Test(
157+
dataProvider = "getInvalidIgnoreFieldsExpressions"
158+
)
159+
public void shouldNotPerformDiffWhenIgnoreFieldsContainsInvalidExpression(String ignoreFieldsExpression, String expectedExceptionMessage) throws JsonProcessingException {
160+
// given
161+
JsonNode source = new ObjectMapper().readTree("{\"a\": \"1\"}");
162+
JsonNode target = new ObjectMapper().readTree("{\"a\": \"1\"}");
163+
List<String> ignoreFields = new ArrayList<>();
164+
ignoreFields.add(ignoreFieldsExpression);
165+
166+
// when
167+
assertThatThrownBy(() -> JsonDiff.asJsonIgnoringFields(source, target, ignoreFields))
168+
.isExactlyInstanceOf(JsonPatchException.class)
169+
.hasMessageStartingWith(expectedExceptionMessage);
170+
171+
assertThatThrownBy(() -> JsonDiff.asJsonPatchIgnoringFields(source, target, ignoreFields))
172+
.isExactlyInstanceOf(JsonPatchException.class)
173+
.hasMessageStartingWith(expectedExceptionMessage);
174+
}
100175
}

0 commit comments

Comments
 (0)