Skip to content

#30 Ignore fields in JsonDiff #31

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jun 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,17 @@ final JsonPatch patch = JsonDiff.asJsonPatch(source, target);
final JsonNode patchNode = JsonDiff.asJson(source, target);
```

It's possible to ignore fields in Json Diff. List of ignored fields should be specified as JsonPointer
or JsonPath paths. If ignored field does not exist in target or source object, it's ignored.

```java
final List<String> fieldsToIgnore = new ArrayList<>();
fieldsToIgnore.add("/id");
fieldsToIgnore.add("$.cars[-1:]");
final JsonPatch patch = JsonDiff.asJsonPatch(source, target, fieldsToIgnore);
final JsonNode patchNode = JsonDiff.asJson(source, target, fieldsToIgnore);
```

**Important note**: the API offers **no guarantee at all** about patch "reuse";
that is, the generated patch is only guaranteed to safely transform the given
source to the given target. Do not expect it to give the result you expect on
Expand Down
102 changes: 95 additions & 7 deletions src/main/java/com/gravity9/jsonpatch/diff/JsonDiff.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,16 +30,23 @@
import com.github.fge.msgsimple.bundle.MessageBundle;
import com.github.fge.msgsimple.load.MessageBundles;
import com.gravity9.jsonpatch.JsonPatch;
import com.gravity9.jsonpatch.JsonPatchException;
import com.gravity9.jsonpatch.JsonPatchMessages;
import com.gravity9.jsonpatch.JsonPatchOperation;
import com.gravity9.jsonpatch.RemoveOperation;
import com.jayway.jsonpath.PathNotFoundException;

import javax.annotation.ParametersAreNonnullByDefault;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import javax.annotation.ParametersAreNonnullByDefault;

/**
* JSON "diff" implementation
Expand Down Expand Up @@ -96,6 +103,57 @@ public static JsonPatch asJsonPatch(final JsonNode source,
return processor.getPatch();
}

/**
* Generate a JSON patch for transforming the source node into the target
* node ignoring given fields
*
* @param source the node to be patched
* @param target the expected result after applying the patch
* @param fieldsToIgnore list of JsonPath or JsonPointer paths which should be ignored when generating diff. Non-existing fields are ignored.
* @return the patch as a {@link JsonPatch}
* @throws JsonPatchException if fieldsToIgnored not in valid JsonPath or JsonPointer format
* @since 2.0.0
*/
public static JsonPatch asJsonPatchIgnoringFields(final JsonNode source,
final JsonNode target, final List<String> fieldsToIgnore) throws JsonPatchException {
BUNDLE.checkNotNull(source, "common.nullArgument");
BUNDLE.checkNotNull(target, "common.nullArgument");
final List<JsonPatchOperation> ignoredFieldsRemoveOperations = getJsonPatchRemoveOperationsForIgnoredFields(fieldsToIgnore);

JsonNode sourceWithoutIgnoredFields = removeIgnoredFields(source, ignoredFieldsRemoveOperations);
JsonNode targetWithoutIgnoredFields = removeIgnoredFields(target, ignoredFieldsRemoveOperations);

final Map<JsonPointer, JsonNode> unchanged
= getUnchangedValues(sourceWithoutIgnoredFields, targetWithoutIgnoredFields);
final DiffProcessor processor = new DiffProcessor(unchanged);

generateDiffs(processor, JsonPointer.empty(), sourceWithoutIgnoredFields, targetWithoutIgnoredFields);
return processor.getPatch();
}

private static JsonNode removeIgnoredFields(JsonNode node, List<JsonPatchOperation> ignoredFieldsRemoveOperations) throws JsonPatchException {
JsonNode nodeWithoutIgnoredFields = node;
for (JsonPatchOperation operation : ignoredFieldsRemoveOperations) {
nodeWithoutIgnoredFields = removeIgnoredFieldOrIgnore(nodeWithoutIgnoredFields, operation);
}
return nodeWithoutIgnoredFields;
}

private static JsonNode removeIgnoredFieldOrIgnore(JsonNode nodeWithoutIgnoredFields, JsonPatchOperation operation) throws JsonPatchException {
try {
List<JsonPatchOperation> operationsList = new ArrayList<>();
operationsList.add(operation);
return new JsonPatch(operationsList).apply(nodeWithoutIgnoredFields);
} catch (JsonPatchException e) {
// If remove for specific path throws PathNotFound, it means that node does not contain specific field which should be ignored.
// See more `empty patch if object does not contain ignored field` in diff.json file.
if (e.getCause() instanceof PathNotFoundException) {
return nodeWithoutIgnoredFields;
}
throw e;
}
}

/**
* Generate a JSON patch for transforming the source node into the target
* node
Expand All @@ -114,6 +172,27 @@ public static JsonNode asJson(final JsonNode source, final JsonNode target) {
}
}

/**
* Generate a JSON patch for transforming the source node into the target
* node ignoring given fields
*
* @param source the node to be patched
* @param target the expected result after applying the patch
* @param fieldsToIgnore list of JsonPath or JsonPointer paths which should be ignored when generating diff. Non-existing fields are ignored.
* @return the patch as a {@link JsonNode}
* @throws JsonPatchException if fieldsToIgnored not in valid JsonPath or JsonPointer format
* @since 2.0.0
*/
public static JsonNode asJsonIgnoringFields(final JsonNode source, final JsonNode target, List<String> fieldsToIgnore) throws JsonPatchException {
final String s;
try {
s = MAPPER.writeValueAsString(asJsonPatchIgnoringFields(source, target, fieldsToIgnore));
return MAPPER.readTree(s);
} catch (IOException e) {
throw new RuntimeException("cannot generate JSON diff", e);
}
}

private static void generateDiffs(final DiffProcessor processor,
final JsonPointer pointer, final JsonNode source, final JsonNode target) {
if (EQUIVALENCE.equivalent(source, target))
Expand Down Expand Up @@ -157,24 +236,24 @@ private static void generateObjectDiffs(final DiffProcessor processor,
final JsonPointer pointer, final ObjectNode source,
final ObjectNode target) {
final Set<String> firstFields
= collect(source.fieldNames(), new TreeSet<String>());
= collect(source.fieldNames(), new TreeSet<>());
final Set<String> secondFields
= collect(target.fieldNames(), new TreeSet<String>());
= collect(target.fieldNames(), new TreeSet<>());

final Set<String> copy1 = new HashSet<String>(firstFields);
final Set<String> copy1 = new HashSet<>(firstFields);
copy1.removeAll(secondFields);

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

final Set<String> copy2 = new HashSet<String>(secondFields);
final Set<String> copy2 = new HashSet<>(secondFields);
copy2.removeAll(firstFields);


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

final Set<String> intersection = new HashSet<String>(firstFields);
final Set<String> intersection = new HashSet<>(firstFields);
intersection.retainAll(secondFields);

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

static Map<JsonPointer, JsonNode> getUnchangedValues(final JsonNode source,
final JsonNode target) {
final Map<JsonPointer, JsonNode> ret = new HashMap<JsonPointer, JsonNode>();
final Map<JsonPointer, JsonNode> ret = new HashMap<>();
computeUnchanged(ret, JsonPointer.empty(), source, target);
return ret;
}
Expand Down Expand Up @@ -278,4 +357,13 @@ private static void computeArray(final Map<JsonPointer, JsonNode> ret,
computeUnchanged(ret, pointer.append(i), source.get(i),
target.get(i));
}

private static List<JsonPatchOperation> getJsonPatchRemoveOperationsForIgnoredFields(List<String> fieldsToIgnore) {
final List<JsonPatchOperation> ignoredFieldsRemoveOperations = new ArrayList<>();
for (String fieldToIgnore : fieldsToIgnore) {
ignoredFieldsRemoveOperations.add(new RemoveOperation(fieldToIgnore));
}
return ignoredFieldsRemoveOperations;
}

}
79 changes: 77 additions & 2 deletions src/test/java/com/gravity9/jsonpatch/diff/JsonDiffTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,23 @@

package com.gravity9.jsonpatch.diff;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.fge.jackson.JsonLoader;
import com.github.fge.jackson.JsonNumEquals;
import com.google.common.collect.Lists;
import com.gravity9.jsonpatch.JsonPatch;
import com.gravity9.jsonpatch.JsonPatchException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

public final class JsonDiffTest {

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

for (final JsonNode node : testData)
list.add(new Object[]{node.get("first"), node.get("second")});
if (!node.has("ignoreFields")) {
list.add(new Object[]{node.get("first"), node.get("second")});
}

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

for (final JsonNode node : testData) {
if (!node.has("patch"))
if (!node.has("patch") || node.has("ignoreFields"))
continue;
list.add(new Object[]{
node.get("message").textValue(), node.get("first"),
Expand All @@ -97,4 +103,73 @@ public void generatedPatchesAreWhatIsExpected(final String message,
+ "expected: %s\nactual: %s\n", message, expected, actual
).isTrue();
}

@DataProvider
public Iterator<Object[]> getDiffsWithIgnoredFields() {
final List<Object[]> list = Lists.newArrayList();

for (final JsonNode node : testData) {
if (node.has("ignoreFields")) {
list.add(new Object[]{
node.get("message").textValue(), node.get("first"),
node.get("second"), node.get("patch"), node.get("ignoreFields")
});
}
}

return list.iterator();
}

@Test(
dataProvider = "getDiffsWithIgnoredFields"
)
public void generatedPatchesIgnoreFields(final String message,
final JsonNode first, final JsonNode second, final JsonNode expected,
final JsonNode ignoreFields) throws JsonPatchException {

final List<String> ignoreFieldsList = new ArrayList<>();
final Iterator<JsonNode> ignoreFieldsIterator = ignoreFields.elements();
while (ignoreFieldsIterator.hasNext()) {
ignoreFieldsList.add(ignoreFieldsIterator.next().textValue());
}

final JsonNode actual = JsonDiff.asJsonIgnoringFields(first, second, ignoreFieldsList);

assertThat(EQUIVALENCE.equivalent(expected, actual)).overridingErrorMessage(
"patch is not what was expected\nscenario: %s\n"
+ "expected: %s\nactual: %s\n", message, expected, actual
).isTrue();
}

@DataProvider
public Iterator<Object[]> getInvalidIgnoreFieldsExpressions() {
final List<Object[]> list = Lists.newArrayList();
list.add(new Object[]{
"$.a[(@.length-1)]", "Could not parse token starting at position 3. Expected ?, ', 0-9, * "
});
list.add(new Object[]{
"/a/?", "Invalid path, `?` are not allowed in JsonPointer expressions."
});
return list.iterator();
}

@Test(
dataProvider = "getInvalidIgnoreFieldsExpressions"
)
public void shouldNotPerformDiffWhenIgnoreFieldsContainsInvalidExpression(String ignoreFieldsExpression, String expectedExceptionMessage) throws JsonProcessingException {
// given
JsonNode source = new ObjectMapper().readTree("{\"a\": \"1\"}");
JsonNode target = new ObjectMapper().readTree("{\"a\": \"1\"}");
List<String> ignoreFields = new ArrayList<>();
ignoreFields.add(ignoreFieldsExpression);

// when
assertThatThrownBy(() -> JsonDiff.asJsonIgnoringFields(source, target, ignoreFields))
.isExactlyInstanceOf(JsonPatchException.class)
.hasMessageStartingWith(expectedExceptionMessage);

assertThatThrownBy(() -> JsonDiff.asJsonPatchIgnoringFields(source, target, ignoreFields))
.isExactlyInstanceOf(JsonPatchException.class)
.hasMessageStartingWith(expectedExceptionMessage);
}
}
Loading