Write JSON unit tests in less code. Great for testing REST interfaces.
—
Pluggable comparison strategies including default comparison logic, custom field-level matching, and specialized array size comparison for different validation scenarios. The comparator system provides the core comparison engine that can be extended and customized for specific testing needs.
Core interface defining the contract for JSON comparison implementations.
public interface JSONComparator {
/**
* Compare two JSONObjects and return detailed results.
*/
JSONCompareResult compareJSON(JSONObject expected, JSONObject actual) throws JSONException;
/**
* Compare two JSONArrays and return detailed results.
*/
JSONCompareResult compareJSON(JSONArray expected, JSONArray actual) throws JSONException;
/**
* Compare JSONObjects with path context, updating result in-place.
*/
void compareJSON(String prefix, JSONObject expected, JSONObject actual, JSONCompareResult result) throws JSONException;
/**
* Compare individual values with path context, updating result in-place.
*/
void compareValues(String prefix, Object expectedValue, Object actualValue, JSONCompareResult result) throws JSONException;
/**
* Compare JSONArrays with path context, updating result in-place.
*/
void compareJSONArray(String prefix, JSONArray expected, JSONArray actual, JSONCompareResult result) throws JSONException;
}Standard implementation providing configurable comparison behavior based on JSONCompareMode settings.
public class DefaultComparator implements JSONComparator {
/**
* Create comparator with specified comparison mode.
*/
public DefaultComparator(JSONCompareMode mode);
public JSONCompareResult compareJSON(JSONObject expected, JSONObject actual) throws JSONException;
public JSONCompareResult compareJSON(JSONArray expected, JSONArray actual) throws JSONException;
public void compareJSON(String prefix, JSONObject expected, JSONObject actual, JSONCompareResult result) throws JSONException;
public void compareValues(String prefix, Object expectedValue, Object actualValue, JSONCompareResult result) throws JSONException;
public void compareJSONArray(String prefix, JSONArray expected, JSONArray actual, JSONCompareResult result) throws JSONException;
}Usage Examples:
// Create comparators with different modes
DefaultComparator strictComparator = new DefaultComparator(JSONCompareMode.STRICT);
DefaultComparator lenientComparator = new DefaultComparator(JSONCompareMode.LENIENT);
// Use directly for programmatic comparison
JSONCompareResult result = strictComparator.compareJSON(expectedObject, actualObject);
// Use with JSONAssert
JSONAssert.assertEquals(expected, actual, lenientComparator);Extends DefaultComparator to support field-specific custom matching through Customization objects.
public class CustomComparator extends DefaultComparator {
/**
* Create custom comparator with base mode and field customizations.
*
* @param mode base comparison mode for non-customized fields
* @param customizations variable number of field-specific customizations
*/
public CustomComparator(JSONCompareMode mode, Customization... customizations);
@Override
public void compareValues(String prefix, Object expectedValue, Object actualValue, JSONCompareResult result) throws JSONException;
}Usage Examples:
// Single customization
RegularExpressionValueMatcher<Object> emailMatcher =
new RegularExpressionValueMatcher<>("^[\\w._%+-]+@[\\w.-]+\\.[A-Z]{2,}$");
Customization emailCustomization = new Customization("user.email", emailMatcher);
CustomComparator emailComparator = new CustomComparator(JSONCompareMode.LENIENT, emailCustomization);
// Multiple customizations
Customization timestampCustomization = new Customization("**.timestamp",
new RegularExpressionValueMatcher<>("^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}Z$"));
Customization idCustomization = new Customization("**.id",
new RegularExpressionValueMatcher<>("^\\d+$"));
CustomComparator multiCustomComparator = new CustomComparator(
JSONCompareMode.LENIENT,
emailCustomization,
timestampCustomization,
idCustomization
);
// Use with assertions
JSONAssert.assertEquals(expected, actual, multiCustomComparator);Base class providing common functionality for comparator implementations.
public abstract class AbstractComparator implements JSONComparator {
/**
* Protected constructor for subclasses.
*/
protected AbstractComparator();
// Provides default implementations of interface methods
// that can be overridden by subclasses
}Specialized comparator that compares arrays by size rather than content, useful for validating array length constraints.
public class ArraySizeComparator extends DefaultComparator {
/**
* Create array size comparator with specified mode.
*/
public ArraySizeComparator(JSONCompareMode mode);
// Compares arrays by length instead of element-by-element comparison
}Usage Examples:
// Validate that arrays have expected lengths
ArraySizeComparator sizeComparator = new ArraySizeComparator(JSONCompareMode.STRICT_ORDER);
// Expected array length is encoded in the expected JSON
String expected = "{\"items\":[1,2,3]}"; // Expects 3 elements
String actual = "{\"items\":[\"a\",\"b\",\"c\"]}"; // Has 3 elements
JSONAssert.assertEquals(expected, actual, sizeComparator); // Passes - same length
// Use with array value matcher for partial size validation
ArrayValueMatcher<Object> sizeArrayMatcher = new ArrayValueMatcher<>(sizeComparator, 0, 2);
Customization sizeCustomization = new Customization("data.arrays.*", sizeArrayMatcher);Utility class providing convenience methods for JSON comparison operations.
public class JSONCompareUtil {
// Static utility methods for common comparison operations
// (Implementation details vary - check source for specific methods)
}// Example: Case-insensitive string comparator
public class CaseInsensitiveComparator extends DefaultComparator {
public CaseInsensitiveComparator(JSONCompareMode mode) {
super(mode);
}
@Override
public void compareValues(String prefix, Object expectedValue, Object actualValue, JSONCompareResult result) throws JSONException {
if (expectedValue instanceof String && actualValue instanceof String) {
String expectedStr = (String) expectedValue;
String actualStr = (String) actualValue;
if (!expectedStr.equalsIgnoreCase(actualStr)) {
result.fail(prefix, expectedValue, actualValue);
}
} else {
super.compareValues(prefix, expectedValue, actualValue, result);
}
}
}
// Usage
CaseInsensitiveComparator caseInsensitive = new CaseInsensitiveComparator(JSONCompareMode.LENIENT);
JSONAssert.assertEquals("{\"name\":\"john\"}", "{\"name\":\"JOHN\"}", caseInsensitive);// Complex validation with nested customizations
public void validateComplexJson() throws JSONException {
// Inner comparator for validating array elements
RegularExpressionValueMatcher<Object> idMatcher = new RegularExpressionValueMatcher<>("^ID-\\d+$");
Customization innerIdCustomization = new Customization("id", idMatcher);
CustomComparator innerComparator = new CustomComparator(JSONCompareMode.LENIENT, innerIdCustomization);
// Array matcher using the inner comparator
ArrayValueMatcher<Object> arrayMatcher = new ArrayValueMatcher<>(innerComparator);
Customization arrayCustomization = new Customization("users", arrayMatcher);
// Outer comparator with email validation and array validation
RegularExpressionValueMatcher<Object> emailMatcher = new RegularExpressionValueMatcher<>("^[\\w._%+-]+@[\\w.-]+\\.[A-Z]{2,}$");
Customization emailCustomization = new Customization("contact.email", emailMatcher);
CustomComparator outerComparator = new CustomComparator(
JSONCompareMode.LENIENT,
arrayCustomization,
emailCustomization
);
String expected = """
{
"contact": {"email": "user@example.com"},
"users": [{"id": "ID-PLACEHOLDER", "name": "test"}]
}""";
String actual = """
{
"contact": {"email": "user@example.com"},
"users": [
{"id": "ID-123", "name": "test", "active": true},
{"id": "ID-456", "name": "test", "role": "admin"}
]
}""";
JSONAssert.assertEquals(expected, actual, outerComparator);
}// Multi-strategy validation approach
public class HybridComparator extends DefaultComparator {
private final ArraySizeComparator sizeComparator;
private final CustomComparator customComparator;
public HybridComparator(JSONCompareMode mode, Customization... customizations) {
super(mode);
this.sizeComparator = new ArraySizeComparator(mode);
this.customComparator = new CustomComparator(mode, customizations);
}
@Override
public void compareJSONArray(String prefix, JSONArray expected, JSONArray actual, JSONCompareResult result) throws JSONException {
// First check array sizes
if (prefix.endsWith(".sizes")) {
sizeComparator.compareJSONArray(prefix, expected, actual, result);
} else if (hasCustomization(prefix)) {
customComparator.compareJSONArray(prefix, expected, actual, result);
} else {
super.compareJSONArray(prefix, expected, actual, result);
}
}
private boolean hasCustomization(String prefix) {
// Logic to determine if path has customizations
return customComparator.hasCustomizationForPath(prefix);
}
}public class ValidatingComparator extends DefaultComparator {
public ValidatingComparator(JSONCompareMode mode) {
super(mode);
}
@Override
public void compareValues(String prefix, Object expectedValue, Object actualValue, JSONCompareResult result) throws JSONException {
try {
// Custom validation logic
if (prefix.contains("date")) {
validateDateFormat(expectedValue, actualValue, result, prefix);
} else if (prefix.contains("url")) {
validateUrlFormat(expectedValue, actualValue, result, prefix);
} else {
super.compareValues(prefix, expectedValue, actualValue, result);
}
} catch (Exception e) {
result.fail(prefix + " validation error: " + e.getMessage());
}
}
private void validateDateFormat(Object expected, Object actual, JSONCompareResult result, String prefix) {
// Custom date validation logic
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
try {
dateFormat.parse(actual.toString());
// Date is valid, consider it a match regardless of expected value
} catch (ParseException e) {
result.fail(prefix, "valid date format", actual);
}
}
private void validateUrlFormat(Object expected, Object actual, JSONCompareResult result, String prefix) {
try {
new URL(actual.toString());
// URL is valid
} catch (MalformedURLException e) {
result.fail(prefix, "valid URL format", actual);
}
}
}// All assertion methods accept JSONComparator
JSONAssert.assertEquals(expected, actual, customComparator);
JSONAssert.assertNotEquals(expected, actual, customComparator);// Direct use with comparison methods
JSONCompareResult result = JSONCompare.compareJSON(expected, actual, customComparator);public class ComparatorFactory {
public static JSONComparator createEmailValidatingComparator() {
RegularExpressionValueMatcher<Object> emailMatcher =
new RegularExpressionValueMatcher<>("^[\\w._%+-]+@[\\w.-]+\\.[A-Z]{2,}$");
Customization emailCustomization = new Customization("**.email", emailMatcher);
return new CustomComparator(JSONCompareMode.LENIENT, emailCustomization);
}
public static JSONComparator createStrictArrayComparator() {
return new DefaultComparator(JSONCompareMode.STRICT_ORDER);
}
public static JSONComparator createSizeOnlyComparator() {
return new ArraySizeComparator(JSONCompareMode.LENIENT);
}
}Install with Tessl CLI
npx tessl i tessl/maven-org-skyscreamer--jsonassert