A customizable library to perform JSON comparisons with detailed diff output.
This library provides:
- Readable diffs between two JSON documents
- Similarity scoring (0-100) to compare multiple JSON documents and find the most similar ones
- Fully customizable comparison modes (strict, lenient, or mixed) with easy-to-create custom matchers
- Multiple output formats (patch file, text) with the ability to create custom formatters
Maven:
<dependency>
<groupId>io.github.deblockt</groupId>
<artifactId>json-diff</artifactId>
<version>2.0.0</version>
</dependency>Gradle:
implementation 'io.github.deblockt:json-diff:2.0.0'Note: Version 2.0.0 requires Java 21+ and uses Jackson 3.x
finalvarexpectedJson = "{\"name\": \"John\", \"age\": 30, \"city\": \"Paris\"}";
finalvarreceivedJson = "{\"name\": \"Jane\", \"age\": 30, \"country\": \"France\"}";
// Define your matcherfinalvarjsonMatcher = newCompositeJsonMatcher(
newLenientJsonArrayPartialMatcher(),
newLenientJsonObjectPartialMatcher(),
newStrictPrimitivePartialMatcher()
);
// Generate the difffinalvardiff = DiffGenerator.diff(expectedJson, receivedJson, jsonMatcher);
// Display errorsSystem.out.println(OnlyErrorDiffViewer.from(diff));
// Get similarity score (0-100)System.out.println("Similarity: " + diff.similarityRate() + "%");Output:
The property "$.city" is not found
The property "$.name" didn't match. Expected "John", Received: "Jane"
Similarity: 50.0%
finalvarerrors = OnlyErrorDiffViewer.from(diff);
System.out.println(errors);Output:
The property "$.city" is not found
The property "$.name" didn't match. Expected "John", Received: "Jane"
finalvarpatch = PatchDiffViewer.from(diff);
System.out.println(patch);Output:
--- actual+++ expected@@ @@
{
"age": 30,
+ "city": "Paris",- "country": "France",- "name": "Jane",+ "name": "John"
}CompositeJsonMatcher accepts multiple matchers that handle different JSON types. The order matters: the first matcher that can handle a comparison will be used.
Requires exact matches:
finalvarstrictMatcher = newCompositeJsonMatcher(
newStrictJsonArrayPartialMatcher(), // Same items in same ordernewStrictJsonObjectPartialMatcher(), // Same properties, no extrasnewStrictPrimitivePartialMatcher() // Exact type and value match
);Ignores extra properties and array order:
finalvarlenientMatcher = newCompositeJsonMatcher(
newLenientJsonArrayPartialMatcher(), // Ignores array order and extra itemsnewLenientJsonObjectPartialMatcher(), // Ignores extra propertiesnewLenientNumberPrimitivePartialMatcher(), // 10.0 == 10newStrictPrimitivePartialMatcher() // Other primitives
);You can combine matchers for custom behavior:
finalvarmixedMatcher = newCompositeJsonMatcher(
newLenientJsonArrayPartialMatcher(), // Lenient on arraysnewStrictJsonObjectPartialMatcher(), // Strict on objectsnewStrictPrimitivePartialMatcher()
);| Matcher | Description |
|---|---|
LenientJsonArrayPartialMatcher | Ignores array order and extra items |
StrictJsonArrayPartialMatcher | Requires same items in same order |
| Matcher | Description |
|---|---|
LenientJsonObjectPartialMatcher | Ignores extra properties in received JSON |
StrictJsonObjectPartialMatcher | Requires exact same properties |
| Matcher | Description |
|---|---|
StrictPrimitivePartialMatcher | Exact type and value match |
LenientNumberPrimitivePartialMatcher | Numbers are equal if values match (10.0 == 10) |
| Matcher | Description |
|---|---|
NullEqualsEmptyArrayMatcher | Treats null and [] as equivalent |
IgnoredPathMatcher | Ignores specified fields during comparison |
The NullEqualsEmptyArrayMatcher allows you to consider null values and empty arrays [] as equivalent. This is useful when different systems represent "no data" differently.
finalvarjsonMatcher = newCompositeJsonMatcher(
newNullEqualsEmptyArrayMatcher(), // Must be first to handle null vs []newLenientJsonArrayPartialMatcher(),
newLenientJsonObjectPartialMatcher(),
newStrictPrimitivePartialMatcher()
);
// These will match with 100% similarity:// {"items": null} vs {"items": []}// {"items": []} vs {"items": null}finalvardiff = DiffGenerator.diff(
"{\"items\": null}",
"{\"items\": []}",
jsonMatcher
);
System.out.println(diff.similarityRate()); // 100.0Important:
- Place
NullEqualsEmptyArrayMatcherbefore other matchers in the constructor - This matcher only handles
nullvs empty array[], not missing properties - Non-empty arrays do not match
null
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.
finalvarjsonMatcher = newCompositeJsonMatcher(
newIgnoredPathMatcher("timestamp", "id"), // Must be firstnewLenientJsonArrayPartialMatcher(),
newLenientJsonObjectPartialMatcher(),
newStrictPrimitivePartialMatcher()
);
// These will match with 100% similarity:finalvardiff = DiffGenerator.diff(
"{\"name\": \"John\", \"timestamp\": \"2024-01-01\"}",
"{\"name\": \"John\", \"timestamp\": \"2024-12-31\"}",
jsonMatcher
);
System.out.println(diff.similarityRate()); // 100.0The 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 |
// Ignore a single field everywherenewIgnoredPathMatcher("createdAt")
// Ignore multiple fieldsnewIgnoredPathMatcher("createdAt", "updatedAt", "id")
// Ignore nested fieldnewIgnoredPathMatcher("metadata.timestamp")
// Ignore field in all array elementsnewIgnoredPathMatcher("users[*].password")
// Combine multiple patternsnewIgnoredPathMatcher("id", "*.createdAt", "items[*].internalId")Important:
- Place
IgnoredPathMatcherbefore other matchers in the constructor - Patterns match against the end of the path, so
namematches$.user.nameas well as$.name
finalvarexpectedJson = """ { "additionalProperty": "a", "foo": "bar", "bar": "bar", "numberMatch": 10.0, "numberUnmatched": 10.01, "arrayMatch": [{"b": "a"}], "arrayUnmatched": [{"b": "a"}] } """;
finalvarreceivedJson = """ { "foo": "foo", "bar": "bar", "numberMatch": 10, "numberUnmatched": 10.02, "arrayMatch": [{"b": "a"}], "arrayUnmatched": {"b": "b"} } """;
finalvarjsonMatcher = newCompositeJsonMatcher(
newLenientJsonArrayPartialMatcher(),
newLenientJsonObjectPartialMatcher(),
newLenientNumberPrimitivePartialMatcher(),
newStrictPrimitivePartialMatcher()
);
finalvardiff = DiffGenerator.diff(expectedJson, receivedJson, jsonMatcher);
System.out.println(OnlyErrorDiffViewer.from(diff));
System.out.println("Similarity: " + diff.similarityRate() + "%");Output:
The property "$.additionalProperty" is not found
The property "$.numberUnmatched" didn't match. Expected 10.01, Received: 10.02
The property "$.arrayUnmatched" didn't match. Expected [{"b":"a"}], Received: {"b":"b"}
The property "$.foo" didn't match. Expected "bar", Received: "foo"
Similarity: 76.0%
You can create custom matchers by implementing the PartialJsonMatcher<T> interface:
publicclassMyCustomMatcherimplementsPartialJsonMatcher<JsonNode> {
@Overridepublicbooleanmanage(JsonNodeexpected, JsonNodereceived) {
// Return true if this matcher should handle this comparisonreturn/* your condition */;
}
@OverridepublicJsonDiffjsonDiff(Pathpath, JsonNodeexpected, JsonNodereceived, JsonMatcherjsonMatcher) {
// Return your diff resultif (/* values match */) {
returnnewMatchedPrimaryDiff(path, expected);
}
returnnewUnMatchedPrimaryDiff(path, expected, received);
}
}This project is licensed under the MIT License.