Skip to content

shend2025/JavaJsonDiff

Repository files navigation

What's this JavaJSONDiff

Core Advantages

  1. Use yaml file to define complex json comparison rules
  2. Can customize rules
  3. Can easily extend new comparison rules based on existing source code, Easier to rewrite and expand new rules
  4. For huge json files, such as json files over several megabytes, has better performance, with jsonpath selector to improve performance

Usage

UltraJSONDiff provides a simple and powerful API for comparing JSON data using YAML configuration rules. The core method is JSONCompare.compareJSON().

Basic Usage

The main method for JSON comparison is:

publicstaticJSONCompareResultcompareJSON(StringexpectedStr, StringactualStr, StringyamlRule) throwsException

Parameters:

  • expectedStr: The expected JSON string
  • actualStr: The actual JSON string to compare against
  • yamlRule: YAML configuration string containing comparison rules

Return Value:

  • JSONCompareResult: Object containing the comparison results and any failures

Quick Start Example

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())}

YAML Rules Configuration (rule_case01.yaml)

- 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: "$"

Test Execution

@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())}

What This Test Demonstrates

  1. Precision Comparison: Location coordinates are compared with tolerance for floating-point precision
  2. Field Ignoring: The queryTimestamp field is completely ignored during comparison
  3. Position Tolerance: Position strings are parsed and compared with tolerance
  4. Array Order Control: ordersStrictOrder requires exact order, while ordersWithoutOrder allows reordering
  5. Key-based Array Matching: Arrays can be matched using specific key fields

Configuration Example

SubRule Configuration

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

Advanced Custom Matcher Example

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

Integration Steps

  1. Create the Matcher Class: Implement one of the matcher interfaces in the org.testtools.jsondiff.matcher package.

  2. Follow Naming Convention: Your class name must end with Matcher (e.g., DateStringMatcher, RangePercentageMatcher).

  3. Implement Required Methods:

    • equal(T actual, T expected): Basic comparison method
    • matcherInit(String param, CompareContext compareContext): Initialization method called by the framework
    • For CustomValueMatcher: Implement equal(String prefix, T actual, T expected, JSONCompareDetailResult result, JSONComparator comparator)
  4. 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"

Best Practices

  1. Parameter Validation: Always validate parameters in matcherInit() method and provide sensible defaults.

  2. Error Handling: Implement proper error handling and fallback behavior for invalid inputs.

  3. Performance: Keep matcher logic efficient, especially for large JSON documents.

  4. Documentation: Provide clear documentation for your matcher's behavior and parameter format.

  5. Testing: Create comprehensive tests for your custom matcher to ensure reliability.

Framework Integration

The framework automatically discovers and instantiates your matcher class using reflection. The class must:

  • Be in the org.testtools.jsondiff.matcher package
  • 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.

Summary

UltraJSONDiff's JSONCompare.compareJSON() method provides a powerful and flexible solution for JSON comparison:

Key Features:

  • 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

When to Use:

  • 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

Method Signature:

publicstaticJSONCompareResultcompareJSON(StringexpectedStr, StringactualStr, StringyamlRule) throwsException

This method is the core of UltraJSONDiff and provides all the functionality needed for sophisticated JSON comparison scenarios.

About

Make JSON Diff easier

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • Java100.0%