Skip to content

Commit 8864b74

Browse files
authored
feat: flexible matcher architecture and null/empty array equivalence (#20)
This release introduces a more flexible matcher architecture and adds the ability to treat `null` and empty arrays `[]` as equivalent. ## New Features - **NullEqualsEmptyArrayMatcher**: New matcher that treats `null` and `[]` as equivalent - **Flexible CompositeJsonMatcher**: Now accepts any number of matchers via varargs - **Simplified LenientNumberPrimitivePartialMatcher**: No longer requires delegation ## Breaking Changes - `CompositeJsonMatcher` constructor changed from 3 fixed parameters to varargs - `PartialJsonMatcher` interface now requires `manage(JsonNode, JsonNode)` method - `LenientNumberPrimitivePartialMatcher` no longer accepts a delegated matcher ## Migration ```java // Before new CompositeJsonMatcher( arrayMatcher, objectMatcher, new LenientNumberPrimitivePartialMatcher(new StrictPrimitivePartialMatcher()) ) // After new CompositeJsonMatcher( new NullEqualsEmptyArrayMatcher(), // Optional: null == [] arrayMatcher, objectMatcher, new LenientNumberPrimitivePartialMatcher(), new StrictPrimitivePartialMatcher() ) ``` Closes #17
1 parent ef013c9 commit 8864b74

17 files changed

Lines changed: 642 additions & 158 deletions

README.md

Lines changed: 221 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,131 +1,274 @@
11
# Java json-diff
22

3-
A customizable lib to perform a json-diff
3+
A customizable library to perform JSON comparisons with detailed diff output.
44

5-
## Why Use json-diff library
5+
## Why Use json-diff?
66

7-
The goal of this library is to provide a readable diff between two json file.
7+
This library provides:
88

9-
In addition to the differential, a similarity score is calculated.
10-
This score can be used to compare several json with each other and find the two most similar.
11-
12-
The way to compare json is completely customisable.
13-
14-
2 way to display diff are provided by default (patch file, text file). And you can easily create your own formatter.
9+
- **Readable diffs** between two JSON documents
10+
- **Similarity scoring** (0-100) to compare multiple JSON documents and find the most similar ones
11+
- **Fully customizable** comparison modes (strict, lenient, or mixed) with easy-to-create custom matchers
12+
- **Multiple output formats** (patch file, text) with the ability to create custom formatters
1513

1614
## Installation
1715

18-
maven:
16+
**Maven:**
1917
```xml
2018
<dependency>
2119
<groupId>io.github.deblockt</groupId>
2220
<artifactId>json-diff</artifactId>
23-
<version>1.1.0</version>
21+
<version>2.0.0</version>
2422
</dependency>
2523
```
2624

27-
gradle:
25+
**Gradle:**
2826
```gradle
29-
implementation 'io.github.deblockt:json-diff:1.1.0'
27+
implementation 'io.github.deblockt:json-diff:2.0.0'
3028
```
3129

32-
## Usage
30+
> **Note:** Version 2.0.0 requires Java 21+ and uses Jackson 3.x
31+
32+
## Quick Start
3333

34-
example:
3534
```java
36-
final var expectedJson = "{\"additionalProperty\":\"a\", \"foo\": \"bar\", \"bar\": \"bar\", \"numberMatch\": 10.0, \"numberUnmatched\": 10.01, \"arrayMatch\": [{\"b\":\"a\"}], \"arrayUnmatched\": [{\"b\":\"a\"}]}";
37-
final var receivedJson = "{\"foo\": \"foo\", \"bar\": \"bar\", \"numberMatch\": 10, \"numberUnmatched\": 10.02, \"arrayMatch\": [{\"b\":\"a\"}], \"arrayUnmatched\": {\"b\":\"b\"}}";
35+
final var expectedJson = "{\"name\": \"John\", \"age\": 30, \"city\": \"Paris\"}";
36+
final var receivedJson = "{\"name\": \"Jane\", \"age\": 30, \"country\": \"France\"}";
3837

39-
// define your matcher
40-
// CompositeJsonMatcher use other matcher to perform matching on objects, list or primitive
38+
// Define your matcher
4139
final var jsonMatcher = new CompositeJsonMatcher(
42-
new LenientJsonArrayPartialMatcher(), // comparing array using lenient mode (ignore array order and extra items)
43-
new LenientJsonObjectPartialMatcher(), // comparing object using lenient mode (ignoring extra properties)
44-
new LenientNumberPrimitivePartialMatcher(new StrictPrimitivePartialMatcher()) // comparing primitive types and manage numbers (100.00 == 100)
40+
new LenientJsonArrayPartialMatcher(),
41+
new LenientJsonObjectPartialMatcher(),
42+
new StrictPrimitivePartialMatcher()
4543
);
4644

47-
// generate a diff
48-
final var jsondiff = DiffGenerator.diff(expectedJson, receivedJson, jsonMatcher);
45+
// Generate the diff
46+
final var diff = DiffGenerator.diff(expectedJson, receivedJson, jsonMatcher);
4947

50-
// use the viewer to collect diff data
51-
final var errorsResult= OnlyErrorDiffViewer.from(jsondiff);
48+
// Display errors
49+
System.out.println(OnlyErrorDiffViewer.from(diff));
5250

53-
// print the diff result
54-
System.out.println(errorsResult);
55-
// print a similarity ratio between expected and received json (0 <= ratio <= 100)
56-
System.out.println(jsondiff.similarityRate());
51+
// Get similarity score (0-100)
52+
System.out.println("Similarity: " + diff.similarityRate() + "%");
5753
```
58-
Result:
54+
55+
Output:
5956
```
60-
The property "$.additionalProperty" is not found
61-
The property "$.numberUnmatched" didn't match. Expected 10.01, Received: 10.02
62-
The property "$.arrayUnmatched" didn't match. Expected [{"b":"a"}], Received: {"b":"b"}
63-
The property "$.foo" didn't match. Expected "bar", Received: "foo"
57+
The property "$.city" is not found
58+
The property "$.name" didn't match. Expected "John", Received: "Jane"
6459
65-
76.0
60+
Similarity: 50.0%
6661
```
6762

68-
You can also generate a patch file using this viewer:
63+
## Output Formats
64+
65+
### Error List (OnlyErrorDiffViewer)
66+
6967
```java
70-
final var patch = PatchDiffViewer.from(jsondiff);
68+
final var errors = OnlyErrorDiffViewer.from(diff);
69+
System.out.println(errors);
70+
```
7171

72-
// use the viewer to collect diff data
73-
final var patchFile= PatchDiffViewer.from(jsondiff);
72+
Output:
73+
```
74+
The property "$.city" is not found
75+
The property "$.name" didn't match. Expected "John", Received: "Jane"
76+
```
7477

75-
// print the diff result
76-
System.out.println(patchFile);
78+
### Patch Format (PatchDiffViewer)
79+
80+
```java
81+
final var patch = PatchDiffViewer.from(diff);
82+
System.out.println(patch);
7783
```
7884

79-
Result:
80-
``` diff
85+
Output:
86+
```diff
8187
--- actual
8288
+++ expected
8389
@@ @@
8490
{
85-
+ "additionalProperty": "a",
86-
"bar": "bar",
87-
- "numberUnmatched": 10.02,
88-
+ "numberUnmatched": 10.01,
89-
- "arrayUnmatched": {"b":"b"},
90-
+ "arrayUnmatched": [{"b":"a"}],
91-
- "foo": "foo",
92-
+ "foo": "bar",
93-
"numberMatch": 10.0,
94-
"arrayMatch": [
95-
{
96-
"b": "a"
97-
}
98-
]
91+
"age": 30,
92+
+ "city": "Paris",
93+
- "country": "France",
94+
- "name": "Jane",
95+
+ "name": "John"
9996
}
10097
```
10198

102-
### Comparison mode
99+
## Comparison Modes
103100

104-
You can use many comparison mode to compare you json:
101+
`CompositeJsonMatcher` accepts multiple matchers that handle different JSON types. The order matters: the first matcher that can handle a comparison will be used.
105102

106-
If you want compare json using *lenient* comparison:
107-
```java
108-
final var fullLenient = new CompositeJsonMatcher(
109-
new LenientJsonArrayPartialMatcher(), // comparing array using lenient mode (ignore array order and extra items)
110-
new LenientJsonObjectPartialMatcher(), // comparing object using lenient mode (ignoring extra properties)
111-
new LenientNumberPrimitivePartialMatcher(new StrictPrimitivePartialMatcher()) // comparing primitive types and manage numbers (100.00 == 100)
103+
### Strict Mode
104+
105+
Requires exact matches:
106+
107+
```java
108+
final var strictMatcher = new CompositeJsonMatcher(
109+
new StrictJsonArrayPartialMatcher(), // Same items in same order
110+
new StrictJsonObjectPartialMatcher(), // Same properties, no extras
111+
new StrictPrimitivePartialMatcher() // Exact type and value match
112112
);
113113
```
114114

115-
If you want compare json using *strict* comparison:
116-
```java
117-
final var strictMatcher = new CompositeJsonMatcher(
118-
new StrictJsonArrayPartialMatcher(), // comparing array using strict mode (object should have same properties/value)
119-
new StrictJsonObjectPartialMatcher(), // comparing object using strict mode (array should have same item on same orders)
120-
new StrictPrimitivePartialMatcher() // comparing primitive types (values should be strictly equals type and value)
115+
### Lenient Mode
116+
117+
Ignores extra properties and array order:
118+
119+
```java
120+
final var lenientMatcher = new CompositeJsonMatcher(
121+
new LenientJsonArrayPartialMatcher(), // Ignores array order and extra items
122+
new LenientJsonObjectPartialMatcher(), // Ignores extra properties
123+
new LenientNumberPrimitivePartialMatcher(), // 10.0 == 10
124+
new StrictPrimitivePartialMatcher() // Other primitives
125+
);
126+
```
127+
128+
### Mixed Mode
129+
130+
You can combine matchers for custom behavior:
131+
132+
```java
133+
final var mixedMatcher = new CompositeJsonMatcher(
134+
new LenientJsonArrayPartialMatcher(), // Lenient on arrays
135+
new StrictJsonObjectPartialMatcher(), // Strict on objects
136+
new StrictPrimitivePartialMatcher()
137+
);
138+
```
139+
140+
## Available Matchers
141+
142+
### Array Matchers
143+
144+
| Matcher | Description |
145+
|---------|-------------|
146+
| `LenientJsonArrayPartialMatcher` | Ignores array order and extra items |
147+
| `StrictJsonArrayPartialMatcher` | Requires same items in same order |
148+
149+
### Object Matchers
150+
151+
| Matcher | Description |
152+
|---------|-------------|
153+
| `LenientJsonObjectPartialMatcher` | Ignores extra properties in received JSON |
154+
| `StrictJsonObjectPartialMatcher` | Requires exact same properties |
155+
156+
### Primitive Matchers
157+
158+
| Matcher | Description |
159+
|---------|-------------|
160+
| `StrictPrimitivePartialMatcher` | Exact type and value match |
161+
| `LenientNumberPrimitivePartialMatcher` | Numbers are equal if values match (`10.0 == 10`) |
162+
163+
### Special Matchers
164+
165+
| Matcher | Description |
166+
|---------|-------------|
167+
| `NullEqualsEmptyArrayMatcher` | Treats `null` and `[]` as equivalent |
168+
169+
## Treating Null as Empty Array
170+
171+
The `NullEqualsEmptyArrayMatcher` allows you to consider `null` values and empty arrays `[]` as equivalent. This is useful when different systems represent "no data" differently.
172+
173+
```java
174+
final var jsonMatcher = new CompositeJsonMatcher(
175+
new NullEqualsEmptyArrayMatcher(), // Must be first to handle null vs []
176+
new LenientJsonArrayPartialMatcher(),
177+
new LenientJsonObjectPartialMatcher(),
178+
new StrictPrimitivePartialMatcher()
121179
);
180+
181+
// These will match with 100% similarity:
182+
// {"items": null} vs {"items": []}
183+
// {"items": []} vs {"items": null}
184+
185+
final var diff = DiffGenerator.diff(
186+
"{\"items\": null}",
187+
"{\"items\": []}",
188+
jsonMatcher
189+
);
190+
191+
System.out.println(diff.similarityRate()); // 100.0
122192
```
123193

124-
You can mix matcher. For example, be lenient on array and strict on object:
125-
```java
126-
final var matcher = new CompositeJsonMatcher(
127-
new LenientJsonArrayPartialMatcher(), // comparing array using lenient mode (ignore array order and extra items)
128-
new StrictJsonObjectPartialMatcher(), // comparing object using strict mode (array should have same item on same orders)
129-
new StrictPrimitivePartialMatcher() // comparing primitive types (values should be strictly equals type and value)
194+
**Important:**
195+
- Place `NullEqualsEmptyArrayMatcher` **before** other matchers in the constructor
196+
- This matcher only handles `null` vs empty array `[]`, not missing properties
197+
- Non-empty arrays do not match `null`
198+
199+
## Advanced Example
200+
201+
```java
202+
final var expectedJson = """
203+
{
204+
"additionalProperty": "a",
205+
"foo": "bar",
206+
"bar": "bar",
207+
"numberMatch": 10.0,
208+
"numberUnmatched": 10.01,
209+
"arrayMatch": [{"b": "a"}],
210+
"arrayUnmatched": [{"b": "a"}]
211+
}
212+
""";
213+
214+
final var receivedJson = """
215+
{
216+
"foo": "foo",
217+
"bar": "bar",
218+
"numberMatch": 10,
219+
"numberUnmatched": 10.02,
220+
"arrayMatch": [{"b": "a"}],
221+
"arrayUnmatched": {"b": "b"}
222+
}
223+
""";
224+
225+
final var jsonMatcher = new CompositeJsonMatcher(
226+
new LenientJsonArrayPartialMatcher(),
227+
new LenientJsonObjectPartialMatcher(),
228+
new LenientNumberPrimitivePartialMatcher(),
229+
new StrictPrimitivePartialMatcher()
130230
);
231+
232+
final var diff = DiffGenerator.diff(expectedJson, receivedJson, jsonMatcher);
233+
234+
System.out.println(OnlyErrorDiffViewer.from(diff));
235+
System.out.println("Similarity: " + diff.similarityRate() + "%");
236+
```
237+
238+
Output:
239+
```
240+
The property "$.additionalProperty" is not found
241+
The property "$.numberUnmatched" didn't match. Expected 10.01, Received: 10.02
242+
The property "$.arrayUnmatched" didn't match. Expected [{"b":"a"}], Received: {"b":"b"}
243+
The property "$.foo" didn't match. Expected "bar", Received: "foo"
244+
245+
Similarity: 76.0%
131246
```
247+
248+
## Creating Custom Matchers
249+
250+
You can create custom matchers by implementing the `PartialJsonMatcher<T>` interface:
251+
252+
```java
253+
public class MyCustomMatcher implements PartialJsonMatcher<JsonNode> {
254+
255+
@Override
256+
public boolean manage(JsonNode expected, JsonNode received) {
257+
// Return true if this matcher should handle this comparison
258+
return /* your condition */;
259+
}
260+
261+
@Override
262+
public JsonDiff jsonDiff(Path path, JsonNode expected, JsonNode received, JsonMatcher jsonMatcher) {
263+
// Return your diff result
264+
if (/* values match */) {
265+
return new MatchedPrimaryDiff(path, expected);
266+
}
267+
return new UnMatchedPrimaryDiff(path, expected, received);
268+
}
269+
}
270+
```
271+
272+
## License
273+
274+
This project is licensed under the MIT License.

0 commit comments

Comments
 (0)