Skip to content

Latest commit

 

History

1,030 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

JSON Compare

A Java library for matching JSONs, with regex, JSONPath, and embedded match tweaks — no glue code required.

Maven Central Build status Coverage Status

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.

1. Features

  • Fluent builder API: JSONCompare.compare(expected, actual).modes(...).assertMatches()
  • Accepts any JSON-convertible input: JsonNode, JSON String, Map, List, POJO
  • Regex matching on field names and values, with \Q…\E quoting 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 JsonComparator for custom field/value matching strategies
  • Throws org.opentest4j.AssertionFailedError — works with JUnit 5, TestNG, and any opentest4j-aware runner

2. Requirements

  • Java 17 or later

3. Installation

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>'

4. Quickstart

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(); // passes

Either 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(); // passes

5. API

The 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.

6. Compare modes

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 fields
  • JSON_ARRAY_NON_EXTENSIBLE — actual arrays must not have extra elements
  • JSON_ARRAY_STRICT_ORDER — array elements must match position-for-position
  • REGEX_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)

7. Regex matching

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();                                                                // passes

Quote unintentional regex characters with \Q…\E:

JSONCompare.compare("""{ "a": "\\\\Qd+\\\\E" }""", """{ "a": "d+" }""")
        .assertMatches();                                                                // passes

For 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();

8. Embedded match tweaks

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

8.1 Negation — !

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.

8.2 Forbid extras — !.*

String expected = """{ "b": "val1", "!.*": ".*" }""";
String actual   = """{ "a": "val2", "b": "val1" }""";
JSONCompare.compare(expected, actual).assertNotMatches();                                // actual has extra fields
String expected = """[ false, "test", 4, "!.*" ]""";
String actual   = """[ 4, false, "test", 1 ]""";
JSONCompare.compare(expected, actual).assertNotMatches();                                // actual has an extra element

8.3 Require extras — .*

String expected = """{ "b": "val1", ".*": ".*" }""";
String actual   = """{ "b": "val1" }""";
JSONCompare.compare(expected, actual).assertNotMatches();                                // actual has NO extra fields

8.4 JSONPath — #(...)

A 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.

9. Differences

Matching is soft: every mismatch is collected, then reported together.

List<String> diffs = JSONCompare.compare(expected, actual).diffs(); // empty on match

assertMatches() 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.

10. Related

  • JTest-Utils — uses json-compare and adds data capture support.

11. License

Released under the Apache License 2.0.

About

A Java library for comparing JSONs

Topics

Resources

Stars

77 stars

Watchers

3 watching

Forks

Releases

Packages

Used by

Contributors

Languages