- A powerful jsondiff tool for developer and test engineer
- rewrite some modules of https://github.com/skyscreamer/JSONassert which is a tool for Test Assert but UltraJSONDiff experts in JSONDiff
- Use yaml file to define complex json comparison rules
- Can customize rules
- Can easily extend new comparison rules based on existing source code, Easier to rewrite and expand new rules
- For huge json files, such as json files over several megabytes, has better performance, with jsonpath selector to improve performance
UltraJSONDiff provides a simple and powerful API for comparing JSON data using YAML configuration rules. The core method is JSONCompare.compareJSON().
The main method for JSON comparison is:
publicstaticJSONCompareResultcompareJSON(StringexpectedStr, StringactualStr, StringyamlRule) throwsException- expectedStr: The expected JSON string
- actualStr: The actual JSON string to compare against
- yamlRule: YAML configuration string containing comparison rules
- JSONCompareResult: Object containing the comparison results and any failures
Here's a simple example based on the unit test:
importorg.testtools.jsondiff.JSONCompare; importorg.testtools.jsondiff.JSONCompareResult; // Test dataStringexpectedJSON = "{\"name\":\"John\",\"age\":30}"; StringactualJSON = "{\"name\":\"John\",\"age\":30}"; Stringrules = "[]"; // Empty rules for basic comparison// Perform comparisonJSONCompareResultresult = JSONCompare.compareJSON(expectedJSON, actualJSON, rules); // Check if comparison was successfulif (result.getFailure().isEmpty()){System.out.println("JSON comparison successful!")} else{System.out.println("JSON comparison failed: " + result.getFailure())}- subRule: jsonPath: $.userextensible: truestrictOrder: trueignoreNull: truefastFail: falsecustomRules: # Apply number precision comparison to age fields - name: NumberPrecisejsonPath: "**.age"param: "newScale=3,roundingMode=3"# Ignore timestamp field during comparison - name: IngorePathparam: "user.queryTimestamp"# Compare position values with tolerance - name: ImprecisePositionjsonPath: ""param: "tolerance=0.01;separator=," - subRule: jsonPath: $.ordersStrictOrderextensible: truestrictOrder: truecustomRules: - name: ArrayWithKeyjsonPath: "$"param: "key=orderId" - subRule: jsonPath: $.ordersWithoutOrderextensible: truestrictOrder: falsecustomRules: - name: ArrayDisorderjsonPath: "$"@TestpublicvoidtestCase01() throwsException{// Read test filesStringexpectedJSON = readFileContent("src/test/resources/case_01_e.json"); StringactualJSON = readFileContent("src/test/resources/case_01_a.json"); Stringrules = readFileContent("src/test/resources/rule_case01.yaml"); // Execute comparisonJSONCompareResultresult = JSONCompare.compareJSON(expectedJSON, actualJSON, rules); // Process resultsObjectMapperobjectMapper = newObjectMapper(); StringactualResult = objectMapper.writeValueAsString(result.getFailure()); // Validate - this test should pass because:// 1. User data matches (with precision tolerance for location.x)// 2. Timestamp is ignored// 3. Position values are compared with tolerance// 4. ordersStrictOrder maintains strict order// 5. ordersWithoutOrder allows reorderingassertTrue("Comparison should succeed with configured rules", result.getFailure().isEmpty())}- Precision Comparison: Location coordinates are compared with tolerance for floating-point precision
- Field Ignoring: The
queryTimestampfield is completely ignored during comparison - Position Tolerance: Position strings are parsed and compared with tolerance
- Array Order Control:
ordersStrictOrderrequires exact order, whileordersWithoutOrderallows reordering - Key-based Array Matching: Arrays can be matched using specific key fields
The following example demonstrates how to configure a subRule for comparing JSON data:
- subRule: # JSONPath expression to specify which part of the JSON to apply this rule tojsonPath: $.user# Whether the target JSON can have additional fields not present in the expected JSONextensible: true# Whether arrays should be compared in strict order (true) or allow reordering (false)strictOrder: true# Whether to ignore null values during comparisonignoreNull: true# Whether to stop comparison immediately when first difference is foundfastFail: falsepreProcess: # TODO# Remove specific nodes from JSON before comparisonremoveNode: jsonPath: ""# Custom comparison rules to apply to this JSONPathcustomRules: # Apply number precision comparison to all age fields (3 decimal places, rounding mode 3) - name: NumberPrecisejsonPath: "**.age"param: "newScale=3,roundingMode=3"# Compare arrays using a specific key field for matching elements - name: ArrayWithKeyjsonPath: "$"param: "key=id"# Ignore specific paths during comparison - name: IngorePathparam: "user.queryTimestamp"# Allow array elements to be in any order - name: ArrayDisorderjsonPath: "**.ordersWithoutOrder"# Recursively compare array elements - name: ArrayRecursivelyjsonPath: param: # Compare degree values with specified tolerance - name: DegreePrecisejsonPath: param: "tolerance=10e-1"# Compare radian values with specified tolerance - name: RadianPrecisejsonPath: param: "tolerance=10e-4"# Compare values with tolerance for floating point differences - name: TolerantValuejsonPath: ""param: "tolerance=10e-4"# Compare values with percentage-based tolerance - name: PercentTolerantjsonPath: ""param: "tolerance=10e-4"# Compare position values with tolerance (e.g.,{"position": "-300.0,-250.0"}) - name: ImprecisePositionjsonPath: ""param: "tolerance=0.01;separator=,"### Configuration Parameters Explained- **jsonPath**: Specifies the JSON path to which this rule applies (e.g., `$.user` targets the user object)- **extensible**: When `true`, allows the target JSON to contain additional fields not present in the expected JSON- **strictOrder**: When `true`, arrays must be in the exact same order; when `false`, array elements can be reordered- **ignoreNull**: When `true`, null values are ignored during comparison- **fastFail**: When `true`, comparison stops immediately when the first difference is found- **preProcess**: Pre-processing options for removing nodes before comparison- **customRules**: Array of custom comparison rules with specific behaviors:- **NumberPrecise**: Compares numbers with specified precision and rounding mode- **ArrayWithKey**: Compares arrays using a specific key field for element matching- **IngorePath**: Ignores specific JSON paths during comparison- **ArrayDisorder**: Allows array elements to be in any order- **ArrayRecursively**: Recursively compares array elements- **DegreePrecise**: Compares degree values with tolerance- **RadianPrecise**: Compares radian values with tolerance- **TolerantValue**: Compares values with tolerance for floating point differences- **PercentTolerant**: Compares values with percentage-based tolerance- **ImprecisePosition**: Compares position values with tolerance## Creating Custom Matcher ClassesUltraJSONDiff provides a flexible framework for creating custom matcher classes to handle specific comparison requirements. This section explains how to create and integrate custom matchers.### Matcher Interface TypesThere are three main interfaces you can implement for custom matchers: 1. **ValueMatcher<T>**: Basic interface for simple value comparison2. **CustomValueMatcher<T>**: Extended interface for complex comparison with detailed failure reporting3. **LocationAwareValueMatcher<T>**: Interface for matchers that need access to JSON path information### Basic Custom Matcher ExampleHere's an example of creating a simple custom matcher that compares date strings in a specific format: ```javapackage org.testtools.jsondiff.matcher;import org.testtools.jsondiff.CompareContext;import java.time.LocalDate;import java.time.format.DateTimeFormatter;/** * Custom matcher for comparing date strings in YYYY-MM-DD format */public class DateStringMatcher<T> implements ValueMatcher<T>{ private DateTimeFormatter formatter; public DateStringMatcher(){ this.formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); } @Override public boolean equal(T actual, T expected){ try{ String actualStr = actual.toString(); String expectedStr = expected.toString(); LocalDate actualDate = LocalDate.parse(actualStr, formatter); LocalDate expectedDate = LocalDate.parse(expectedStr, formatter); return actualDate.equals(expectedDate); } catch (Exception e){ // If parsing fails, fall back to string comparison return actual.equals(expected); } } @Override public void matcherInit(String param, CompareContext compareContext){ // Parse custom date format from parameter if provided if (param != null && !param.trim().isEmpty()){ try{ this.formatter = DateTimeFormatter.ofPattern(param.trim()); } catch (Exception e){ // Keep default format if parameter is invalid } } }}For more complex scenarios requiring detailed failure reporting, implement CustomValueMatcher:
packageorg.testtools.jsondiff.matcher; importorg.testtools.jsondiff.CompareContext; importorg.testtools.jsondiff.JSONCompareDetailResult; importorg.testtools.jsondiff.comparator.JSONComparator; importorg.testtools.jsondiff.comparator.JSONCompareUtil; importjava.math.BigDecimal; importjava.util.Objects; /** * Custom matcher for comparing numeric ranges with percentage tolerance */publicclassRangePercentageMatcher<T> implementsCustomValueMatcher<T>{privatedoublepercentageTolerance; publicRangePercentageMatcher(){this.percentageTolerance = 5.0; // Default 5% tolerance } @Overridepublicbooleanequal(Tactual, Texpected){// Basic implementation for simple comparisonreturnequal(null, actual, expected, null, null)} @Overridepublicbooleanequal(Stringprefix, Tactual, Texpected, JSONCompareDetailResultresult, JSONComparatorcomparator) throwsValueMatcherException{try{BigDecimalactualNum = newBigDecimal(actual.toString()); BigDecimalexpectedNum = newBigDecimal(expected.toString()); // Calculate percentage differenceBigDecimaldiff = actualNum.subtract(expectedNum).abs(); BigDecimalpercentageDiff = diff.divide(expectedNum, 4, BigDecimal.ROUND_HALF_UP) .multiply(BigDecimal.valueOf(100)); booleanisWithinTolerance = percentageDiff.compareTo(BigDecimal.valueOf(percentageTolerance)) <= 0; if (!isWithinTolerance && result != null){// Add detailed failure informationresult.fail(prefix, String.format("Expected value within %.2f%% tolerance", percentageTolerance), String.format("Actual: %s, Expected: %s, Difference: %.2f%%", actual, expected, percentageDiff.doubleValue()))} returnisWithinTolerance} catch (NumberFormatExceptione){// Fall back to exact comparison for non-numeric valuesreturnactual.equals(expected)} } @OverridepublicvoidmatcherInit(Stringparam, CompareContextcompareContext){if (param != null && !param.trim().isEmpty()){try{this.percentageTolerance = Double.parseDouble( Objects.requireNonNull(JSONCompareUtil.getParamValue(param)))} catch (Exceptione){// Keep default tolerance if parameter is invalid } } } }Create the Matcher Class: Implement one of the matcher interfaces in the
org.testtools.jsondiff.matcherpackage.Follow Naming Convention: Your class name must end with
Matcher(e.g.,DateStringMatcher,RangePercentageMatcher).Implement Required Methods:
equal(T actual, T expected): Basic comparison methodmatcherInit(String param, CompareContext compareContext): Initialization method called by the framework- For
CustomValueMatcher: Implementequal(String prefix, T actual, T expected, JSONCompareDetailResult result, JSONComparator comparator)
Use in YAML Configuration: Reference your matcher by name (without the "Matcher" suffix):
- subRule: jsonPath: "$.dates"customRules: - name: DateStringjsonPath: "**.date"param: "yyyy-MM-dd" - name: RangePercentagejsonPath: "**.score"param: "tolerance=10.0"Parameter Validation: Always validate parameters in
matcherInit()method and provide sensible defaults.Error Handling: Implement proper error handling and fallback behavior for invalid inputs.
Performance: Keep matcher logic efficient, especially for large JSON documents.
Documentation: Provide clear documentation for your matcher's behavior and parameter format.
Testing: Create comprehensive tests for your custom matcher to ensure reliability.
The framework automatically discovers and instantiates your matcher class using reflection. The class must:
- Be in the
org.testtools.jsondiff.matcherpackage - Have a public default constructor
- Follow the naming convention:
{YourMatcherName}Matcher - Implement the required interface methods
Your custom matcher will be automatically available for use in YAML configuration files without any additional registration steps.
UltraJSONDiff's JSONCompare.compareJSON() method provides a powerful and flexible solution for JSON comparison:
- Simple API: Single method call with three parameters
- YAML Configuration: Declarative rule definition for complex comparison scenarios
- Flexible Matching: Support for tolerance, precision, array ordering, and field ignoring
- Extensible: Easy to add custom comparison rules
- Performance: Optimized for large JSON files with JSONPath selectors
- API Testing: Compare API responses with expected results
- Data Validation: Verify JSON data transformations
- Integration Testing: Ensure data consistency across systems
- Regression Testing: Detect changes in JSON output formats
publicstaticJSONCompareResultcompareJSON(StringexpectedStr, StringactualStr, StringyamlRule) throwsExceptionThis method is the core of UltraJSONDiff and provides all the functionality needed for sophisticated JSON comparison scenarios.