A Java library for matching JSONs, with regex, JSONPath, and embedded match tweaks — no glue code required.
Compare any two JSON-convertible Java objects and report every difference on
mismatch. The expected JSON is matched leniently by default — sizes and array
order are ignored, scalar values are matched as Java regular expressions, and
match logic can be tuned with operators (!, !.*, .*, #(...)) embedded
directly inside the expected JSON.
- Fluent builder API:
JSONCompare.compare(expected, actual).modes(...).assertMatches() - Accepts any JSON-convertible input:
JsonNode, JSONString,Map,List, POJO - Regex matching on field names and values, with
\Q…\Equoting to escape literals - Embedded match operators: negation (
!), forbid-extras (!.*), require-extras (.*) - Embedded JSONPath assertions via
#(<jsonpath>)keys, powered by Jayway JsonPath - Soft assertions: every mismatch is reported, not just the first
- Pluggable
JsonComparatorfor custom field/value matching strategies - Throws
org.opentest4j.AssertionFailedError— works with JUnit 5, TestNG, and any opentest4j-aware runner
- Java 17 or later
Maven:
<dependency>
<groupId>com.github.fslev</groupId>
<artifactId>json-compare</artifactId>
<version>${version.from.maven.central}</version>
</dependency>Gradle:
testImplementation 'com.github.fslev:json-compare:<version.from.maven.central>'String expected = """
{
"string": "I'm on a seafood diet. I see food and I eat it!",
"number": "\\\\d+.\\\\d+",
"object": {
"pun": "\\\\QWhy don't skeletons fight each other? They don't have the guts!\\\\E"
},
"array": [".*", "\\\\d+", true, null],
"boolean": "true|false"
}
""";
String actual = """
{
"string": "I'm on a seafood diet. I see food and I eat it!",
"number": 0.99,
"object": {
"pun": "Why don't skeletons fight each other? They don't have the guts!"
},
"array": ["pancake", 18, true, null],
"boolean": true
}
""";
JSONCompare.compare(expected, actual).assertMatches(); // passesEither side may be any JSON-convertible object — Jackson handles the conversion:
Map<String, Object> actualMap = new HashMap<>();
actualMap.put("a", 1);
actualMap.put("b", Arrays.asList("ipsum", 4, 5));
actualMap.put("c", true);
String expected = """
{ "a": 1, "b": [4, "ipsum", "\\\\d+"] }
""";
JSONCompare.compare(expected, actualMap).assertMatches(); // passesThe fluent builder is the only entry point. Configuration methods return
this; the comparison runs only when a terminal method is invoked.
| Builder method | Purpose |
|---|---|
.modes(CompareMode... | Set<…>) |
Tighten default lenient comparison (see below) |
.comparator(JsonComparator) |
Override the default regex-based field/value comparator |
.message(String) |
Append a custom note to assertion failure output |
.assertMatches() (terminal) |
Throw AssertionFailedError if any difference is found |
.assertNotMatches() (terminal) |
Throw AssertionFailedError if the JSONs do match |
.diffs() (terminal) |
Return a List<String> of differences (empty on match) |
The static JSONCompare.assertMatches(...) / assertNotMatches(...) /
diffs(...) overloads still work but are deprecated since 8.0; they delegate
to the builder.
By default, expected JSON only has to be a subset of actual — extra fields
and array elements are tolerated and array order is ignored. The
CompareMode enum tightens this:
JSON_OBJECT_NON_EXTENSIBLE— actual objects must not have extra fieldsJSON_ARRAY_NON_EXTENSIBLE— actual arrays must not have extra elementsJSON_ARRAY_STRICT_ORDER— array elements must match position-for-positionREGEX_DISABLED— compare values and field names by literal equality
String expected = """{ "b": "val1" }""";
String actual = """{ "a": "val2", "b": "val1" }""";
JSONCompare.compare(expected, actual).assertMatches(); // passes (subset)
JSONCompare.compare(expected, actual)
.modes(CompareMode.JSON_OBJECT_NON_EXTENSIBLE)
.assertNotMatches(); // passes (extras present)String expected = """[ "lorem", 2, false ]""";
String actual = """[ false, 2, "lorem", 5, 4 ]""";
JSONCompare.compare(expected, actual).assertMatches(); // passes (order ignored)
JSONCompare.compare(expected, actual)
.modes(CompareMode.JSON_ARRAY_STRICT_ORDER)
.assertNotMatches(); // passes (order differs)Expected scalar values and field names are interpreted as Java regular
expressions (with DOTALL | MULTILINE). Patterns that fail to compile fall
back to literal equality.
JSONCompare.compare("""{ "a": ".*me.*" }""", """{ "a": "some text" }""")
.assertMatches(); // passes
JSONCompare.compare("""{ ".*oba.*": "some value" }""", """{ "foobar": "some value" }""")
.assertMatches(); // passesQuote unintentional regex characters with \Q…\E:
JSONCompare.compare("""{ "a": "\\\\Qd+\\\\E" }""", """{ "a": "d+" }""")
.assertMatches(); // passesFor case-insensitivity, use the inline (?i) / (?-i) flags.
To disable regex globally, use CompareMode.REGEX_DISABLED or supply a custom
JsonComparator:
JSONCompare.compare(expected, actual)
.comparator(new JsonComparator() {
public boolean compareValues(Object expected, Object actual) { return expected.equals(actual); }
public boolean compareFields(String expected, String actual) { return expected.equals(actual); }
})
.assertMatches();Match logic can be embedded directly inside the expected JSON, so assertions can express conditions without writing any matcher code:
| Operator | Meaning |
|---|---|
!<regex> |
DO NOT MATCH — negate the comparison for this value/field |
!.* |
DO NOT MATCH ANY — actual must NOT have extra fields/items |
.* |
MATCH ANY — actual MUST have at least one extra field/item |
#(<jsonpath>) |
Apply a JSONPath query and match the result against the value |
JSONCompare.compare("""{ "a": "!test" }""", """{ "a": "testing" }""")
.assertMatches(); // passes — value does not match "test"
JSONCompare.compare("""{ "!a": "value does not matter" }""", """{ "b": "anything" }""")
.assertMatches(); // passes — actual has no field "a"When negating a field name, the value side is ignored: it asserts only that no actual field with the given name exists at that level.
String expected = """{ "b": "val1", "!.*": ".*" }""";
String actual = """{ "a": "val2", "b": "val1" }""";
JSONCompare.compare(expected, actual).assertNotMatches(); // actual has extra fieldsString expected = """[ false, "test", 4, "!.*" ]""";
String actual = """[ 4, false, "test", 1 ]""";
JSONCompare.compare(expected, actual).assertNotMatches(); // actual has an extra elementString expected = """{ "b": "val1", ".*": ".*" }""";
String actual = """{ "b": "val1" }""";
JSONCompare.compare(expected, actual).assertNotMatches(); // actual has NO extra fieldsA field whose name has the form #(<jsonpath>) runs the path against actual
and matches the result against the field's value:
String expected = """
{
"#($.store..isbn)": [ "0-395-19395-8", "0-553-21311-3", "!.*" ]
}
""";
// actual: a typical { "store": { "book": [ ... ] } } document
JSONCompare.compare(expected, actual).assertMatches();The example asserts the store contains exactly two specific ISBNs — the
trailing "!.*" forbids any further results.
Matching is soft: every mismatch is collected, then reported together.
List<String> diffs = JSONCompare.compare(expected, actual).diffs(); // empty on matchassertMatches() formats the same diffs into an AssertionFailedError:
org.opentest4j.AssertionFailedError: FOUND 4 DIFFERENCE(S):
_________________________DIFF__________________________
$.caught
Expected value: false But got: true
_________________________DIFF__________________________
$.pain.range[1] was not found:
"blue"
_________________________DIFF__________________________
$.pain.range[2] was not found:
-2059921070
_________________________DIFF__________________________
$.pain.not_anyone was not found
When an expected object or array inside an array is not found, the diff also shows the closest actual element that no other expected element matched, named by its path in the actual JSON, and how it differs:
$.users[0] was not found:
{
"id" : 42,
"name" : "Alice",
"zip" : "10115"
}
Closest unmatched actual element $.users[1] differs by:
- .zip
Expected value: "10115" But got: "10117"
When every actual element is already matched, the diff says the expected element might be extra:
$.tags[1] was not found:
"x"
No unmatched actual elements left in $.tags, so this expected element might be extra
Add a custom note with .message("…") — it is appended to the failure
output.
- JTest-Utils — uses json-compare and adds data capture support.
Released under the Apache License 2.0.