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
4 changes: 2 additions & 2 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@ jobs:

steps:
- uses: actions/checkout@v3
- name: Set up JDK 11
- name: Set up JDK 21
uses: actions/setup-java@v3
with:
java-version: '11'
java-version: '21'
distribution: 'temurin'
server-id: github
settings-path: ${{ github.workspace }}
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ jobs:

steps:
- uses: actions/checkout@v3
- name: Set up JDK 11
- name: Set up JDK 21
uses: actions/setup-java@v3
with:
java-version: '11'
java-version: '21'
distribution: 'temurin'
- name: Setup gradle
uses: gradle/gradle-build-action@67421db6bd0bf253fb4bd25b31ebb98943c375e1
Expand Down
61 changes: 61 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Build Commands

```bash
# Build the project
./gradlew build

# Run all tests
./gradlew test

# Run a single test class
./gradlew test --tests "com.deblock.jsondiff.matcher.LenientJsonArrayPartialMatcherTest"

# Run a single test method
./gradlew test --tests "com.deblock.jsondiff.DiffGeneratorTest.singleDiff"

# Create JAR
./gradlew jar

# Publish to Maven local
./gradlew publishToMavenLocal
```

## Architecture

This library compares two JSON documents and produces a diff with a similarity score (0-100).

### Core Components

**Entry Point:** `DiffGenerator.diff(expectedJson, actualJson, jsonMatcher)` parses JSON strings and delegates to matchers.

**Matcher Layer** (`com.deblock.jsondiff.matcher`):
- `CompositeJsonMatcher` - Combines different matchers for objects, arrays, and primitives
- `PartialJsonMatcher<T>` - Interface for type-specific matching strategies
- Two modes available:
- **Strict:** `StrictJsonObjectPartialMatcher`, `StrictJsonArrayPartialMatcher`, `StrictPrimitivePartialMatcher`
- **Lenient:** `LenientJsonObjectPartialMatcher` (ignores extra properties), `LenientJsonArrayPartialMatcher` (ignores order/extra items), `LenientNumberPrimitivePartialMatcher` (10.0 == 10)

**Diff Representation** (`com.deblock.jsondiff.diff`):
- `JsonDiff` - Interface with `similarityRate()` and `display(viewer)` methods
- `JsonObjectDiff` - Object comparison result (similarity = 60% structure + 40% values)
- `JsonArrayDiff` - Array comparison result (similarity = average of matched items)
- `MatchedPrimaryDiff` / `UnMatchedPrimaryDiff` - Primitive comparison results

**Viewer Layer** (`com.deblock.jsondiff.viewer`):
- `JsonDiffViewer` - Visitor interface for consuming diff results
- `OnlyErrorDiffViewer` - Human-readable error list output
- `PatchDiffViewer` - Unified diff format output

### Design Patterns

- **Visitor Pattern:** JsonDiffViewer consumes JsonDiff via `display(viewer)` method
- **Strategy Pattern:** Different PartialJsonMatcher implementations for different comparison modes
- **Composite Pattern:** CompositeJsonMatcher combines object/array/primitive matchers

### Path Tracking

`Path` class tracks JSON location (e.g., `$.property.0.subproperty`) using `PathItem` subclasses (`ObjectProperty`, `ArrayIndex`).
11 changes: 6 additions & 5 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,10 @@ repositories {
}

dependencies {
implementation 'com.fasterxml.jackson.core:jackson-databind:2.15.2'
testImplementation 'org.junit.jupiter:junit-jupiter:5.7.1'
testImplementation 'org.mockito:mockito-core:3.8.0'
implementation 'tools.jackson.core:jackson-databind:3.0.3'
testImplementation platform('org.junit:junit-bom:5.10.2')
testImplementation 'org.junit.jupiter:junit-jupiter'
testImplementation 'org.mockito:mockito-core:5.21.0'
}

group = 'io.github.deblockt'
Expand Down Expand Up @@ -50,8 +51,8 @@ java {
withJavadocJar()
withSourcesJar()

sourceCompatibility = JavaLanguageVersion.of(11)
targetCompatibility = JavaLanguageVersion.of(11)
sourceCompatibility = JavaLanguageVersion.of(21)
targetCompatibility = JavaLanguageVersion.of(21)
}

test {
Expand Down
8 changes: 4 additions & 4 deletions src/main/java/com/deblock/jsondiff/DiffGenerator.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
import com.deblock.jsondiff.diff.JsonDiff;
import com.deblock.jsondiff.matcher.JsonMatcher;
import com.deblock.jsondiff.matcher.Path;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import tools.jackson.core.JacksonException;
import tools.jackson.databind.JsonNode;
import tools.jackson.databind.ObjectMapper;

import java.util.List;
import java.util.stream.Collectors;
Expand All @@ -27,7 +27,7 @@ public static List<JsonDiff> diff(String expected, List<String> actualValues, Js
private static JsonNode read(String json) {
try {
return OBJECT_MAPPER.readTree(json);
} catch (JsonProcessingException e) {
} catch (JacksonException e) {
throw new JsonReadException(e);
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/com/deblock/jsondiff/diff/JsonArrayDiff.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import com.deblock.jsondiff.matcher.Path;
import com.deblock.jsondiff.viewer.JsonDiffViewer;
import com.fasterxml.jackson.databind.JsonNode;
import tools.jackson.databind.JsonNode;

import java.util.HashMap;
import java.util.Map;
Expand Down
4 changes: 2 additions & 2 deletions src/main/java/com/deblock/jsondiff/diff/JsonObjectDiff.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

import com.deblock.jsondiff.matcher.Path;
import com.deblock.jsondiff.viewer.JsonDiffViewer;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import tools.jackson.databind.JsonNode;
import tools.jackson.databind.node.ObjectNode;

import java.util.HashMap;
import java.util.Map;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import com.deblock.jsondiff.matcher.Path;
import com.deblock.jsondiff.viewer.JsonDiffViewer;
import com.fasterxml.jackson.databind.JsonNode;
import tools.jackson.databind.JsonNode;

public class MatchedPrimaryDiff implements JsonDiff {
private final JsonNode value;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import com.deblock.jsondiff.matcher.Path;
import com.deblock.jsondiff.viewer.JsonDiffViewer;
import com.fasterxml.jackson.databind.JsonNode;
import tools.jackson.databind.JsonNode;

public class UnMatchedPrimaryDiff implements JsonDiff {
private final JsonNode expectedValue;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
package com.deblock.jsondiff.matcher;

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

public class CompositeJsonMatcher implements JsonMatcher {
private final PartialJsonMatcher<ArrayNode> jsonArrayPartialMatcher;
Expand All @@ -23,12 +23,12 @@ public CompositeJsonMatcher(

@Override
public JsonDiff diff(Path path, JsonNode expected, JsonNode received) {
if (expected instanceof ObjectNode && received instanceof ObjectNode) {
return this.jsonObjectPartialMatcher.jsonDiff(path, (ObjectNode) expected, (ObjectNode) received, this);
} else if (expected instanceof ArrayNode && received instanceof ArrayNode) {
return this.jsonArrayPartialMatcher.jsonDiff(path, (ArrayNode) expected, (ArrayNode) received, this);
} else if (expected instanceof ValueNode && received instanceof ValueNode){
return this.primitivePartialMatcher.jsonDiff(path, (ValueNode) expected, (ValueNode) received, this);
if (expected instanceof ObjectNode expectedObjectNode && received instanceof ObjectNode receivedObjectNode) {
return this.jsonObjectPartialMatcher.jsonDiff(path, expectedObjectNode, receivedObjectNode, this);
} else if (expected instanceof ArrayNode expectedArrayNode && received instanceof ArrayNode receivedArrayNode) {
return this.jsonArrayPartialMatcher.jsonDiff(path, expectedArrayNode, receivedArrayNode, this);
} else if (expected instanceof ValueNode expectedValueNode && received instanceof ValueNode receivedValueNode){
return this.primitivePartialMatcher.jsonDiff(path, expectedValueNode, receivedValueNode, this);
} else {
return new UnMatchedPrimaryDiff(path, expected, received);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package com.deblock.jsondiff.matcher;

import com.deblock.jsondiff.diff.*;
import com.fasterxml.jackson.databind.JsonNode;
import tools.jackson.databind.JsonNode;

public interface JsonMatcher {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,10 @@

import com.deblock.jsondiff.diff.JsonArrayDiff;
import com.deblock.jsondiff.diff.JsonDiff;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import tools.jackson.databind.JsonNode;
import tools.jackson.databind.node.ArrayNode;

import java.util.*;
import java.util.stream.Collectors;

public class LenientJsonArrayPartialMatcher implements PartialJsonMatcher<ArrayNode> {
Expand Down Expand Up @@ -99,9 +93,9 @@ private MismatchPair<List<IndexedJsonNode>, List<IndexedJsonNode>> processMatchi
return new MismatchPair<>(expectedMissing, actualMissing);
}

private NodeCounter getElementsWithCount(Iterator<JsonNode> elements) {
private NodeCounter getElementsWithCount(Collection<JsonNode> elements) {
var nodeCounter = new NodeCounter();
elements.forEachRemaining(nodeCounter::addNode);
elements.forEach(nodeCounter::addNode);
return nodeCounter;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,16 @@

import com.deblock.jsondiff.diff.JsonDiff;
import com.deblock.jsondiff.diff.JsonObjectDiff;
import com.fasterxml.jackson.databind.node.ObjectNode;
import tools.jackson.databind.node.ObjectNode;

public class LenientJsonObjectPartialMatcher implements PartialJsonMatcher<ObjectNode> {

@Override
public JsonDiff jsonDiff(Path path, ObjectNode expectedJson, ObjectNode receivedJson, JsonMatcher jsonMatcher) {
final var jsonDiff = new JsonObjectDiff(path);

expectedJson.fields()
.forEachRemaining(entry -> {
expectedJson.properties()
.forEach(entry -> {
final var expectedPropertyName = entry.getKey();
final var expectedValue = entry.getValue();
final var receivedValue = receivedJson.get(expectedPropertyName);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
import com.deblock.jsondiff.diff.JsonDiff;
import com.deblock.jsondiff.diff.MatchedPrimaryDiff;
import com.deblock.jsondiff.diff.UnMatchedPrimaryDiff;
import com.fasterxml.jackson.databind.node.NumericNode;
import com.fasterxml.jackson.databind.node.ValueNode;
import tools.jackson.databind.node.NumericNode;
import tools.jackson.databind.node.ValueNode;

public class LenientNumberPrimitivePartialMatcher implements PartialJsonMatcher<ValueNode> {
private final PartialJsonMatcher<ValueNode> delegated;
Expand All @@ -16,12 +16,7 @@ public LenientNumberPrimitivePartialMatcher(PartialJsonMatcher<ValueNode> delega
@Override
public JsonDiff jsonDiff(Path path, ValueNode expectedValue, ValueNode receivedValue, JsonMatcher jsonMatcher) {
if (expectedValue instanceof NumericNode && receivedValue instanceof NumericNode) {
final var expectedIntValue = expectedValue.intValue();
final var actualIntValue = receivedValue.intValue();
final var expectedDecimalValue = receivedValue.doubleValue() % 1;
final var actualDecimalValue = expectedValue.doubleValue() % 1;

if (expectedIntValue != actualIntValue || expectedDecimalValue != actualDecimalValue) {
if (expectedValue.decimalValue().compareTo(receivedValue.decimalValue()) != 0) {
return new UnMatchedPrimaryDiff(path, expectedValue, receivedValue);
} else {
return new MatchedPrimaryDiff(path, expectedValue);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package com.deblock.jsondiff.matcher;

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

public interface PartialJsonMatcher<T extends JsonNode> {
JsonDiff jsonDiff(Path path, T expectedJson, T receivedJson, JsonMatcher jsonMatcher);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import com.deblock.jsondiff.diff.JsonArrayDiff;
import com.deblock.jsondiff.diff.JsonDiff;
import com.fasterxml.jackson.databind.node.ArrayNode;
import tools.jackson.databind.node.ArrayNode;

import java.util.Comparator;
import java.util.HashMap;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,16 @@

import com.deblock.jsondiff.diff.JsonDiff;
import com.deblock.jsondiff.diff.JsonObjectDiff;
import com.fasterxml.jackson.databind.node.ObjectNode;

import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
import tools.jackson.databind.node.ObjectNode;

public class StrictJsonObjectPartialMatcher implements PartialJsonMatcher<ObjectNode> {

@Override
public JsonDiff jsonDiff(Path path, ObjectNode expectedJson, ObjectNode receivedJson, JsonMatcher jsonMatcher) {
final var jsonDiff = new JsonObjectDiff(path);
final var receivedJsonFields = StreamSupport.stream(((Iterable<String>) receivedJson::fieldNames).spliterator(), false).collect(Collectors.toSet());

expectedJson.fields()
.forEachRemaining(entry -> {
expectedJson.properties()
.forEach(entry -> {
final var expectedPropertyName = entry.getKey();
final var expectedValue = entry.getValue();
final var receivedValue = receivedJson.get(expectedPropertyName);
Expand All @@ -26,12 +22,10 @@ public JsonDiff jsonDiff(Path path, ObjectNode expectedJson, ObjectNode received
final var diff = jsonMatcher.diff(path.add(Path.PathItem.of(expectedPropertyName)), expectedValue, receivedValue);
jsonDiff.addPropertyDiff(expectedPropertyName, diff);
}
receivedJsonFields.remove(expectedPropertyName);
});


receivedJson.fields()
.forEachRemaining(entry -> {
receivedJson.properties()
.forEach(entry -> {
final var receivedPropertyName = entry.getKey();
final var receivedPropertyValue = entry.getValue();
final var expectedValue = expectedJson.get(receivedPropertyName);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import com.deblock.jsondiff.diff.JsonDiff;
import com.deblock.jsondiff.diff.MatchedPrimaryDiff;
import com.deblock.jsondiff.diff.UnMatchedPrimaryDiff;
import com.fasterxml.jackson.databind.node.ValueNode;
import tools.jackson.databind.node.ValueNode;

import java.util.Objects;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import com.deblock.jsondiff.diff.JsonDiff;
import com.deblock.jsondiff.matcher.Path;
import com.fasterxml.jackson.databind.JsonNode;
import tools.jackson.databind.JsonNode;

public interface JsonDiffViewer {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import com.deblock.jsondiff.diff.JsonDiff;
import com.deblock.jsondiff.matcher.Path;
import com.fasterxml.jackson.databind.JsonNode;
import tools.jackson.databind.JsonNode;

/**
* List all error on a string
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import com.deblock.jsondiff.diff.JsonDiff;
import com.deblock.jsondiff.matcher.Path;
import com.fasterxml.jackson.databind.JsonNode;
import tools.jackson.databind.JsonNode;

import java.util.ArrayList;
import java.util.HashMap;
Expand Down
Loading
Loading