Skip to content
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
59 changes: 59 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ final var mixedMatcher = new CompositeJsonMatcher(
| Matcher | Description |
|---------|-------------|
| `NullEqualsEmptyArrayMatcher` | Treats `null` and `[]` as equivalent |
| `IgnoredPathMatcher` | Ignores specified fields during comparison |

## Treating Null as Empty Array

Expand Down Expand Up @@ -196,6 +197,64 @@ System.out.println(diff.similarityRate()); // 100.0
- This matcher only handles `null` vs empty array `[]`, not missing properties
- Non-empty arrays do not match `null`

## Ignoring path

The `IgnoredPathMatcher` allows you to ignore specific fields during comparison. This is useful for fields like timestamps, IDs, or other dynamic values that you don't want to compare.

```java
final var jsonMatcher = new CompositeJsonMatcher(
new IgnoredPathMatcher("timestamp", "id"), // Must be first
new LenientJsonArrayPartialMatcher(),
new LenientJsonObjectPartialMatcher(),
new StrictPrimitivePartialMatcher()
);

// These will match with 100% similarity:
final var diff = DiffGenerator.diff(
"{\"name\": \"John\", \"timestamp\": \"2024-01-01\"}",
"{\"name\": \"John\", \"timestamp\": \"2024-12-31\"}",
jsonMatcher
);

System.out.println(diff.similarityRate()); // 100.0
```

### Path Patterns

The `IgnoredPathMatcher` supports various path patterns:

| Pattern | Description | Example |
|---------|-------------|---------|
| `name` | Matches field `name` at any level | Ignores `$.name`, `$.user.name`, `$.data.user.name` |
| `user.name` | Matches `name` under `user` | Ignores `$.user.name`, `$.data.user.name` |
| `*.name` | Wildcard for any property | Ignores `$.foo.name`, `$.bar.name` |
| `items[0]` | Matches specific array index | Ignores `$.items[0]` |
| `items[*]` | Wildcard for any array index | Ignores `$.items[0]`, `$.items[1]`, etc. |
| `items[*].id` | Field in any array element | Ignores `$.items[0].id`, `$.items[5].id` |

### Examples

```java
// Ignore a single field everywhere
new IgnoredPathMatcher("createdAt")

// Ignore multiple fields
new IgnoredPathMatcher("createdAt", "updatedAt", "id")

// Ignore nested field
new IgnoredPathMatcher("metadata.timestamp")

// Ignore field in all array elements
new IgnoredPathMatcher("users[*].password")

// Combine multiple patterns
new IgnoredPathMatcher("id", "*.createdAt", "items[*].internalId")
```

**Important:**
- Place `IgnoredPathMatcher` **before** other matchers in the constructor
- Patterns match against the end of the path, so `name` matches `$.user.name` as well as `$.name`

## Advanced Example

```java
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,6 @@

import com.deblock.jsondiff.diff.*;
import tools.jackson.databind.JsonNode;
import tools.jackson.databind.node.ArrayNode;
import tools.jackson.databind.node.ObjectNode;
import tools.jackson.databind.node.ValueNode;

import java.util.ArrayList;
import java.util.Arrays;
Expand All @@ -21,7 +18,7 @@ public CompositeJsonMatcher(PartialJsonMatcher<?> ...jsonArrayPartialMatcher) {
@Override
public JsonDiff diff(Path path, JsonNode expected, JsonNode received) {
return this.matchers.stream()
.filter(matcher -> matcher.manage(expected, received))
.filter(matcher -> matcher.manage(path, received, expected))
.findFirst()
.map(matcher -> matcher.jsonDiff(path, expected, received, this))
.orElseGet(() -> new UnMatchedPrimaryDiff(path, expected, received));
Expand Down
32 changes: 32 additions & 0 deletions src/main/java/com/deblock/jsondiff/matcher/IgnoredPathMatcher.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.deblock.jsondiff.matcher;

import com.deblock.jsondiff.diff.JsonDiff;
import com.deblock.jsondiff.diff.MatchedPrimaryDiff;
import tools.jackson.databind.JsonNode;

import java.util.Arrays;
import java.util.List;

public class IgnoredPathMatcher implements PartialJsonMatcher {
private final List<PathMatcher> pathsToIgnore;

public IgnoredPathMatcher(List<String> paths) {
this.pathsToIgnore = paths.stream()
.map(PathMatcher::from)
.toList();
}

public IgnoredPathMatcher(String ...paths) {
this(Arrays.stream(paths).toList());
}

@Override
public JsonDiff jsonDiff(Path path, JsonNode expectedJson, JsonNode receivedJson, JsonMatcher jsonMatcher) {
return new MatchedPrimaryDiff(path, expectedJson);
}

@Override
public boolean manage(Path path, JsonNode expected, JsonNode received) {
return pathsToIgnore.stream().anyMatch(pattern -> pattern.match(path));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ public JsonDiff jsonDiff(Path path, ArrayNode expectedArrayNode, ArrayNode recie
}

@Override
public boolean manage(JsonNode expected, JsonNode received) {
public boolean manage(Path path, JsonNode received, JsonNode expected) {
return expected.isArray() && received.isArray();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public JsonDiff jsonDiff(Path path, ObjectNode expectedJson, ObjectNode received
}

@Override
public boolean manage(JsonNode expected, JsonNode received) {
public boolean manage(Path path, JsonNode received, JsonNode expected) {
return expected.isObject() && received.isObject();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public JsonDiff jsonDiff(Path path, ValueNode expectedValue, ValueNode receivedV
}

@Override
public boolean manage(JsonNode expected, JsonNode received) {
public boolean manage(Path path, JsonNode received, JsonNode expected) {
return expected.isNumber() && received.isNumber();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public JsonDiff jsonDiff(Path path, JsonNode expectedJson, JsonNode receivedJson
}

@Override
public boolean manage(JsonNode expected, JsonNode received) {
public boolean manage(Path path, JsonNode received, JsonNode expected) {
return (expected.isNull() && received.isArray())
|| (received.isNull() && expected.isArray());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@
public interface PartialJsonMatcher<T extends JsonNode> {
JsonDiff jsonDiff(Path path, T expectedJson, T receivedJson, JsonMatcher jsonMatcher);

boolean manage(JsonNode expected, JsonNode received);
boolean manage(Path path, JsonNode received, JsonNode expected);

}
72 changes: 55 additions & 17 deletions src/main/java/com/deblock/jsondiff/matcher/Path.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,50 +2,88 @@

import java.util.Objects;

/**
* Represents a JSON path (e.g., $.property.0.subproperty).
* Stored in reverse order (last element at head) for O(1) add operations
* and efficient end-matching in PathMatcher.
*/
public class Path {
public static final Path ROOT = new Path();

public final PathItem property;
public final Path next;
private final PathItem last;
private final Path previous;

public Path() {
this(null, null);
}

private Path(PathItem property, Path next) {
this.property = property;
this.next = next;
private Path(PathItem last, Path previous) {
this.last = last;
this.previous = previous;
}

private Path(PathItem property) {
this.property = property;
this.next = null;
public Path add(PathItem item) {
if (this.last == null) {
return new Path(item, null);
}
return new Path(item, this);
}

public Path add(PathItem item) {
if (this.next == null) {
return new Path(this.property, new Path(item));
} else {
return new Path(this.property, this.next.add(item));
public PathItem item() {
return last;
}

/**
* Returns the path without its last element.
*/
public Path previous() {
return previous == null ? ROOT : previous;
}

/**
* Returns the path items in natural order (from root to leaf).
* This is useful for traversing the path from start to end.
*/
public java.util.List<PathItem> toList() {
java.util.List<PathItem> result = new java.util.ArrayList<>();
collectItems(result);
return result;
}

private void collectItems(java.util.List<PathItem> result) {
if (last == null) return;
if (previous != null) {
previous.collectItems(result);
}
result.add(last);
}

@Override
public String toString() {
return ((this.property == null) ? "$" : this.property) +
((this.next == null) ? "" : "." + this.next);
StringBuilder sb = new StringBuilder("$");
appendReversed(sb);
return sb.toString();
}

private void appendReversed(StringBuilder sb) {
if (last == null) return;
if (previous != null) {
previous.appendReversed(sb);
}
sb.append(".").append(last);
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Path path = (Path) o;
return Objects.equals(property, path.property) && Objects.equals(next, path.next);
return Objects.equals(last, path.last) && Objects.equals(previous, path.previous);
}

@Override
public int hashCode() {
return Objects.hash(property, next);
return Objects.hash(last, previous);
}

public interface PathItem {
Expand Down
Loading
Loading