• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

fslev / json-compare / #682

30 Apr 2026 11:29AM UTC coverage: 97.158% (-0.5%) from 97.612%
#682

push

web-flow
Merge f7f38eebc into 4691206c7

262 of 272 new or added lines in 13 files covered. (96.32%)

1 existing line in 1 file now uncovered.

376 of 387 relevant lines covered (97.16%)

0.97 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

92.16
/src/main/java/io/json/compare/ComparisonBuilder.java
1
package io.json.compare;
2

3
import com.fasterxml.jackson.databind.JsonNode;
4
import io.json.compare.matcher.JsonMatcher;
5
import io.json.compare.util.JsonUtils;
6
import org.opentest4j.AssertionFailedError;
7

8
import java.io.IOException;
9
import java.util.Arrays;
10
import java.util.EnumSet;
11
import java.util.List;
12
import java.util.Set;
13

14
/**
15
 * Fluent entry point to the json-compare engine. Obtained via
16
 * {@link JSONCompare#compare(Object, Object)}.
17
 *
18
 * <p>Configuration methods return {@code this} for chaining; terminal methods
19
 * ({@link #assertMatches()}, {@link #assertNotMatches()}, {@link #diffs()})
20
 * trigger the actual comparison.
21
 *
22
 * <p><b>Mutation contract.</b> The builder stores {@code expected},
23
 * {@code actual}, {@code comparator} and {@code modes} as plain references and
24
 * does not defensively copy them. Mutating any of these between a configuration
25
 * call and a terminal call produces undefined behavior.
26
 *
27
 * <p>Example:
28
 * <pre>{@code
29
 *   JSONCompare.compare(expected, actual)
30
 *       .modes(CompareMode.JSON_OBJECT_NON_EXTENSIBLE)
31
 *       .message("users endpoint")
32
 *       .assertMatches();
33
 * }</pre>
34
 */
35
public final class ComparisonBuilder {
36

37
    private static final String LS = System.lineSeparator();
1✔
38
    private static final String DIFF_DIVIDER = "_________________________DIFF__________________________";
39
    private static final String ASSERTION_ERROR_HINT_MESSAGE =
1✔
40
            "Json matching is by default case-sensitive and uses regular expressions." + LS
41
                    + "In case expected json contains any unintentional regexes, then quote them between \\Q and \\E delimiters.\n"
42
                    + "For disabling case-sensitivity, use (?i) and (?-i) modifiers. Or, use a custom comparator.";
43

44
    private final Object expected;
45
    private final Object actual;
46
    private JsonComparator comparator;
47
    private Set<CompareMode> modes;
48
    private String message;
49

50
    ComparisonBuilder(Object expected, Object actual) {
1✔
51
        this.expected = expected;
1✔
52
        this.actual = actual;
1✔
53
    }
1✔
54

55
    /**
56
     * Sets a custom comparator (defaults to {@link DefaultJsonComparator}).
57
     */
58
    public ComparisonBuilder comparator(JsonComparator comparator) {
59
        this.comparator = comparator;
1✔
60
        return this;
1✔
61
    }
62

63
    /**
64
     * Sets compare modes (replaces any previously set modes).
65
     */
66
    public ComparisonBuilder modes(Set<CompareMode> modes) {
67
        this.modes = modes;
1✔
68
        return this;
1✔
69
    }
70

71
    /**
72
     * Sets compare modes (replaces any previously set modes). Null-safe.
73
     */
74
    public ComparisonBuilder modes(CompareMode... modes) {
NEW
75
        this.modes = (modes == null || modes.length == 0) ? null : EnumSet.copyOf(Arrays.asList(modes));
×
NEW
76
        return this;
×
77
    }
78

79
    /**
80
     * Appends a user-supplied message to the assertion failure output.
81
     */
82
    public ComparisonBuilder message(String message) {
83
        this.message = message;
1✔
84
        return this;
1✔
85
    }
86

87
    /**
88
     * Asserts that expected and actual match, throwing
89
     * {@link AssertionFailedError} with every difference on failure.
90
     */
91
    public void assertMatches() {
92
        JsonNode expectedJson = toJson(expected);
1✔
93
        JsonNode actualJson = toJson(actual);
1✔
94
        List<String> diffs = runMatch(expectedJson, actualJson);
1✔
95
        if (diffs.isEmpty()) {
1✔
96
            return;
1✔
97
        }
98
        String defaultMessage = buildFailureMessage(diffs);
1✔
99
        String finalMessage = message == null ? defaultMessage : defaultMessage + LS + message;
1✔
100
        throw new AssertionFailedError(finalMessage, prettyPrint(expectedJson), prettyPrint(actualJson));
1✔
101
    }
102

103
    /**
104
     * Asserts that expected and actual DO NOT match, throwing
105
     * {@link AssertionFailedError} when they unexpectedly do.
106
     */
107
    public void assertNotMatches() {
108
        JsonNode expectedJson = toJson(expected);
1✔
109
        JsonNode actualJson = toJson(actual);
1✔
110
        List<String> diffs = runMatch(expectedJson, actualJson);
1✔
111
        if (!diffs.isEmpty()) {
1✔
112
            return;
1✔
113
        }
114
        String defaultMessage = LS + "JSONs are equal";
1✔
115
        String finalMessage = message == null ? defaultMessage : defaultMessage + LS + message;
1✔
116
        throw new AssertionFailedError(finalMessage, prettyPrint(expectedJson), prettyPrint(actualJson));
1✔
117
    }
118

119
    /**
120
     * Returns the differences as a list of JSONPath-prefixed strings. The
121
     * returned list is empty if the comparison matched.
122
     */
123
    public List<String> diffs() {
124
        JsonNode expectedJson = toJson(expected);
1✔
125
        JsonNode actualJson = toJson(actual);
1✔
126
        List<String> diffs = runMatch(expectedJson, actualJson);
1✔
127
        return diffs.stream().map(d -> "$" + d).toList();
1✔
128
    }
129

130
    private List<String> runMatch(JsonNode expectedJson, JsonNode actualJson) {
131
        JsonComparator effectiveComparator = comparator != null ? comparator : new DefaultJsonComparator(modes);
1✔
132
        return new JsonMatcher(expectedJson, actualJson, effectiveComparator, modes).match();
1✔
133
    }
134

135
    private String buildFailureMessage(List<String> diffs) {
136
        StringBuilder sb = new StringBuilder(128 + diffs.size() * 64);
1✔
137
        sb.append("FOUND ").append(diffs.size()).append(" DIFFERENCE(S):").append(LS);
1✔
138
        for (String diff : diffs) {
1✔
139
            sb.append(LS).append(LS).append(DIFF_DIVIDER).append(LS).append('$').append(diff);
1✔
140
        }
1✔
141
        sb.append(LS);
1✔
142
        if (comparator == null || comparator.getClass() == DefaultJsonComparator.class) {
1✔
143
            sb.append(LS).append(LS).append(ASSERTION_ERROR_HINT_MESSAGE).append(LS);
1✔
144
        }
145
        return sb.toString();
1✔
146
    }
147

148
    private static JsonNode toJson(Object obj) {
149
        try {
150
            return JsonUtils.toJson(obj);
1✔
151
        } catch (IOException e) {
1✔
152
            throw new RuntimeException("Invalid JSON" + LS + e + LS);
1✔
153
        }
154
    }
155

156
    private static String prettyPrint(JsonNode jsonNode) {
157
        try {
158
            return JsonUtils.prettyPrint(jsonNode);
1✔
NEW
159
        } catch (IOException e) {
×
NEW
160
            throw new RuntimeException(e);
×
161
        }
162
    }
163
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc