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
299 changes: 221 additions & 78 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,131 +1,274 @@
# Java json-diff

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

## Why Use json-diff library
## Why Use json-diff?

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

In addition to the differential, a similarity score is calculated.
This score can be used to compare several json with each other and find the two most similar.

The way to compare json is completely customisable.

2 way to display diff are provided by default (patch file, text file). And you can easily create your own formatter.
- **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

## Installation

maven:
**Maven:**
```xml
<dependency>
<groupId>io.github.deblockt</groupId>
<artifactId>json-diff</artifactId>
<version>1.1.0</version>
<version>2.0.0</version>
</dependency>
```

gradle:
**Gradle:**
```gradle
implementation 'io.github.deblockt:json-diff:1.1.0'
implementation 'io.github.deblockt:json-diff:2.0.0'
```

## Usage
> **Note:** Version 2.0.0 requires Java 21+ and uses Jackson 3.x

## Quick Start

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

// define your matcher
// CompositeJsonMatcher use other matcher to perform matching on objects, list or primitive
// Define your matcher
final var jsonMatcher = new CompositeJsonMatcher(
new LenientJsonArrayPartialMatcher(), // comparing array using lenient mode (ignore array order and extra items)
new LenientJsonObjectPartialMatcher(), // comparing object using lenient mode (ignoring extra properties)
new LenientNumberPrimitivePartialMatcher(new StrictPrimitivePartialMatcher()) // comparing primitive types and manage numbers (100.00 == 100)
new LenientJsonArrayPartialMatcher(),
new LenientJsonObjectPartialMatcher(),
new StrictPrimitivePartialMatcher()
);

// generate a diff
final var jsondiff = DiffGenerator.diff(expectedJson, receivedJson, jsonMatcher);
// Generate the diff
final var diff = DiffGenerator.diff(expectedJson, receivedJson, jsonMatcher);

// use the viewer to collect diff data
final var errorsResult= OnlyErrorDiffViewer.from(jsondiff);
// Display errors
System.out.println(OnlyErrorDiffViewer.from(diff));

// print the diff result
System.out.println(errorsResult);
// print a similarity ratio between expected and received json (0 <= ratio <= 100)
System.out.println(jsondiff.similarityRate());
// Get similarity score (0-100)
System.out.println("Similarity: " + diff.similarityRate() + "%");
```
Result:

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"
The property "$.city" is not found
The property "$.name" didn't match. Expected "John", Received: "Jane"

76.0
Similarity: 50.0%
```

You can also generate a patch file using this viewer:
## Output Formats

### Error List (OnlyErrorDiffViewer)

```java
final var patch = PatchDiffViewer.from(jsondiff);
final var errors = OnlyErrorDiffViewer.from(diff);
System.out.println(errors);
```

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

// print the diff result
System.out.println(patchFile);
### Patch Format (PatchDiffViewer)

```java
final var patch = PatchDiffViewer.from(diff);
System.out.println(patch);
```

Result:
``` diff
Output:
```diff
--- actual
+++ expected
@@ @@
{
+ "additionalProperty": "a",
"bar": "bar",
- "numberUnmatched": 10.02,
+ "numberUnmatched": 10.01,
- "arrayUnmatched": {"b":"b"},
+ "arrayUnmatched": [{"b":"a"}],
- "foo": "foo",
+ "foo": "bar",
"numberMatch": 10.0,
"arrayMatch": [
{
"b": "a"
}
]
"age": 30,
+ "city": "Paris",
- "country": "France",
- "name": "Jane",
+ "name": "John"
}
```

### Comparison mode
## Comparison Modes

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

If you want compare json using *lenient* comparison:
```java
final var fullLenient = new CompositeJsonMatcher(
new LenientJsonArrayPartialMatcher(), // comparing array using lenient mode (ignore array order and extra items)
new LenientJsonObjectPartialMatcher(), // comparing object using lenient mode (ignoring extra properties)
new LenientNumberPrimitivePartialMatcher(new StrictPrimitivePartialMatcher()) // comparing primitive types and manage numbers (100.00 == 100)
### Strict Mode

Requires exact matches:

```java
final var strictMatcher = new CompositeJsonMatcher(
new StrictJsonArrayPartialMatcher(), // Same items in same order
new StrictJsonObjectPartialMatcher(), // Same properties, no extras
new StrictPrimitivePartialMatcher() // Exact type and value match
);
```

If you want compare json using *strict* comparison:
```java
final var strictMatcher = new CompositeJsonMatcher(
new StrictJsonArrayPartialMatcher(), // comparing array using strict mode (object should have same properties/value)
new StrictJsonObjectPartialMatcher(), // comparing object using strict mode (array should have same item on same orders)
new StrictPrimitivePartialMatcher() // comparing primitive types (values should be strictly equals type and value)
### Lenient Mode

Ignores extra properties and array order:

```java
final var lenientMatcher = new CompositeJsonMatcher(
new LenientJsonArrayPartialMatcher(), // Ignores array order and extra items
new LenientJsonObjectPartialMatcher(), // Ignores extra properties
new LenientNumberPrimitivePartialMatcher(), // 10.0 == 10
new StrictPrimitivePartialMatcher() // Other primitives
);
```

### Mixed Mode

You can combine matchers for custom behavior:

```java
final var mixedMatcher = new CompositeJsonMatcher(
new LenientJsonArrayPartialMatcher(), // Lenient on arrays
new StrictJsonObjectPartialMatcher(), // Strict on objects
new StrictPrimitivePartialMatcher()
);
```

## Available Matchers

### Array Matchers

| Matcher | Description |
|---------|-------------|
| `LenientJsonArrayPartialMatcher` | Ignores array order and extra items |
| `StrictJsonArrayPartialMatcher` | Requires same items in same order |

### Object Matchers

| Matcher | Description |
|---------|-------------|
| `LenientJsonObjectPartialMatcher` | Ignores extra properties in received JSON |
| `StrictJsonObjectPartialMatcher` | Requires exact same properties |

### Primitive Matchers

| Matcher | Description |
|---------|-------------|
| `StrictPrimitivePartialMatcher` | Exact type and value match |
| `LenientNumberPrimitivePartialMatcher` | Numbers are equal if values match (`10.0 == 10`) |

### Special Matchers

| Matcher | Description |
|---------|-------------|
| `NullEqualsEmptyArrayMatcher` | Treats `null` and `[]` as equivalent |

## Treating Null as Empty Array

The `NullEqualsEmptyArrayMatcher` allows you to consider `null` values and empty arrays `[]` as equivalent. This is useful when different systems represent "no data" differently.

```java
final var jsonMatcher = new CompositeJsonMatcher(
new NullEqualsEmptyArrayMatcher(), // Must be first to handle null vs []
new LenientJsonArrayPartialMatcher(),
new LenientJsonObjectPartialMatcher(),
new StrictPrimitivePartialMatcher()
);

// These will match with 100% similarity:
// {"items": null} vs {"items": []}
// {"items": []} vs {"items": null}

final var diff = DiffGenerator.diff(
"{\"items\": null}",
"{\"items\": []}",
jsonMatcher
);

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

You can mix matcher. For example, be lenient on array and strict on object:
```java
final var matcher = new CompositeJsonMatcher(
new LenientJsonArrayPartialMatcher(), // comparing array using lenient mode (ignore array order and extra items)
new StrictJsonObjectPartialMatcher(), // comparing object using strict mode (array should have same item on same orders)
new StrictPrimitivePartialMatcher() // comparing primitive types (values should be strictly equals type and value)
**Important:**
- Place `NullEqualsEmptyArrayMatcher` **before** other matchers in the constructor
- This matcher only handles `null` vs empty array `[]`, not missing properties
- Non-empty arrays do not match `null`

## Advanced Example

```java
final var expectedJson = """
{
"additionalProperty": "a",
"foo": "bar",
"bar": "bar",
"numberMatch": 10.0,
"numberUnmatched": 10.01,
"arrayMatch": [{"b": "a"}],
"arrayUnmatched": [{"b": "a"}]
}
""";

final var receivedJson = """
{
"foo": "foo",
"bar": "bar",
"numberMatch": 10,
"numberUnmatched": 10.02,
"arrayMatch": [{"b": "a"}],
"arrayUnmatched": {"b": "b"}
}
""";

final var jsonMatcher = new CompositeJsonMatcher(
new LenientJsonArrayPartialMatcher(),
new LenientJsonObjectPartialMatcher(),
new LenientNumberPrimitivePartialMatcher(),
new StrictPrimitivePartialMatcher()
);

final var diff = 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%
```

## Creating Custom Matchers

You can create custom matchers by implementing the `PartialJsonMatcher<T>` interface:

```java
public class MyCustomMatcher implements PartialJsonMatcher<JsonNode> {

@Override
public boolean manage(JsonNode expected, JsonNode received) {
// Return true if this matcher should handle this comparison
return /* your condition */;
}

@Override
public JsonDiff jsonDiff(Path path, JsonNode expected, JsonNode received, JsonMatcher jsonMatcher) {
// Return your diff result
if (/* values match */) {
return new MatchedPrimaryDiff(path, expected);
}
return new UnMatchedPrimaryDiff(path, expected, received);
}
}
```

## License

This project is licensed under the MIT License.
Loading
Loading