Back to API Design Best Practices
Q13Question 13 of 22

How do you compare two JSON strings logically rather than as raw strings

Key Concepts
  • Raw-string comparison fails because JSON objects are unordered, whitespace differs, and value formatting varies.
  • Parse both strings into Jackson JsonNode trees with ObjectMapper.readTree and compare with JsonNode.equals for deep structural equality.
  • JsonNode.equals compares objects regardless of key order but compares arrays by position (array order is significant).
  • A numeric subtlety: Jackson treats IntNode and DoubleNode as different types, so 50000 and 50000.0 are not equal by default.
  • JSONAssert provides LENIENT mode (ignores extras and array order) and STRICT mode for test assertions.
  • zjsonpatch computes a field-level JSON Patch diff, useful for audit and change tracking.

Definition

Parse both JSON documents into trees and compare the trees, because raw-string comparison fails on key order, whitespace, and formatting even when the data is identical.
  • Raw-string comparison – comparing JSON as plain text fails because JSON objects are unordered by definition, whitespace and formatting differ, and value representation varies (e.g. 50000 vs 50000.0).
  • Tree comparison – parse both strings into JsonNode trees with ObjectMapper.readTree and compare with JsonNode.equals, which performs deep structural equality ignoring key order.
  • JSONAssert – a library for test assertions with LENIENT mode (ignores extra fields and array order) and STRICT mode (exact match).
  • zjsonpatch – computes a JSON Patch (RFC 6902) diff between two documents, showing exactly which paths changed.

Rajesh fills out two copies of the same deposit slip. On the first he writes the amount first and the account second; on the second he writes the account first and the amount second. Both slips say the same thing, but comparing them letter by letter they look different. A sensible clerk reads the meaning: same amount, same account, so they match. Comparing JSON logically is reading the meaning; comparing raw strings is the foolish letter-by-letter check.

Understanding the Concepts

Why does raw-string comparison fail?

Raw-string comparison fails for three reasons. First, key order: JSON objects are unordered by definition, so {"amount":50000,"account":"SAV2001"} and {"account":"SAV2001","amount":50000} are the same object but different strings. Second, whitespace: pretty-printed JSON differs from compact JSON. Third, value formatting: 50000, 50000.0, and 5.0E4 are the same number but different text.

JAVA
package com.bank.util;

public class RawComparisonFails {
    public static void main(String[] args) {
        String json1 = "{\"amount\":50000,\"account\":\"SAV2001\"}";
        String json2 = "{\"account\":\"SAV2001\",\"amount\":50000}";

        System.out.println(json1.equals(json2)); // false – but data is identical!
    }
}
TEXT
false

How do you compare with Jackson JsonNode?

Parse both strings into trees with ObjectMapper.readTree and compare with JsonNode.equals, which performs deep, structural, order-independent comparison.

JAVA
package com.bank.util;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;

public class TreeComparison {
    public static void main(String[] args) throws Exception {
        ObjectMapper mapper = new ObjectMapper();
        String json1 = "{\"amount\":50000,\"account\":\"SAV2001\"}";
        String json2 = "{\"account\":\"SAV2001\",\"amount\":50000}";

        JsonNode tree1 = mapper.readTree(json1);
        JsonNode tree2 = mapper.readTree(json2);

        System.out.println(tree1.equals(tree2)); // true – structural equality
    }
}
TEXT
true

What is the numeric subtlety?

By default Jackson treats IntNode(50000) and DoubleNode(50000.0) as different node types, so they are not equal under strict comparison.

JAVA
String json1 = "{\"balance\":50000}";
String json2 = "{\"balance\":50000.00}";

JsonNode tree1 = mapper.readTree(json1);  // IntNode
JsonNode tree2 = mapper.readTree(json2);  // DoubleNode

System.out.println(tree1.equals(tree2));  // false!
TEXT
false

To get numeric equality, use a numeric-aware comparator:

JAVA
import java.util.Comparator;

Comparator<JsonNode> numericComparator = (a, b) -> {
    if (a.isNumber() && b.isNumber()) {
        return a.decimalValue().compareTo(b.decimalValue()) == 0 ? 0 : 1;
    }
    return a.equals(b) ? 0 : 1;
};

boolean same = tree1.equals(numericComparator, tree2); // true

How do you use JSONAssert for testing?

JSONAssert provides configurable assertion modes. LENIENT ignores extra fields and array order. STRICT requires exact match.

JAVA
package com.bank.test;

import org.skyscreamer.jsonassert.JSONAssert;
import org.skyscreamer.jsonassert.JSONCompareMode;

public class JsonAssertDemo {
    public static void main(String[] args) throws Exception {
        String expected = "{\"accountId\":\"SAV2001\",\"balance\":150000}";
        String actual = "{\"balance\":150000,\"accountId\":\"SAV2001\",\"branchCode\":\"NBI-001\"}";

        // LENIENT: ignores extra fields and key order – passes
        JSONAssert.assertEquals(expected, actual, JSONCompareMode.LENIENT);

        // STRICT: requires exact fields – fails because of extra branchCode
        // JSONAssert.assertEquals(expected, actual, JSONCompareMode.STRICT); // fails
    }
}

How do you compute a field-level diff?

For audit and change tracking, zjsonpatch computes a JSON Patch (RFC 6902) diff showing exactly which paths changed.

JAVA
package com.bank.util;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.flipkart.zjsonpatch.JsonDiff;

public class DiffDemo {
    public static void main(String[] args) throws Exception {
        ObjectMapper mapper = new ObjectMapper();
        JsonNode before = mapper.readTree("{\"balance\":150000,\"status\":\"ACTIVE\"}");
        JsonNode after  = mapper.readTree("{\"balance\":100000,\"status\":\"ACTIVE\"}");

        JsonNode diff = JsonDiff.asJson(before, after);
        System.out.println(diff.toPrettyString());
    }
}
TEXT
[ { "op": "replace", "path": "/balance", "value": 100000 } ]

Can you compare by deserializing to typed objects?

Yes. Deserialize both JSON strings into the same typed class with a proper equals method and compare the objects.

JAVA
Payment p1 = mapper.readValue(json1, Payment.class);
Payment p2 = mapper.readValue(json2, Payment.class);
boolean same = p1.equals(p2); // uses record or custom equals

The trade-off is that extra fields not in the class are ignored, which may or may not be what you want.

What about canonical JSON for cryptographic signatures?

For signature verification, byte-exact comparison matters. Canonicalize both documents to a sorted, whitespace-free form, then compare.

JAVA
// Canonical JSON: sort keys, strip whitespace
ObjectMapper canonical = new ObjectMapper()
        .configure(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS, true);
String c1 = canonical.writeValueAsString(canonical.readTree(json1));
String c2 = canonical.writeValueAsString(canonical.readTree(json2));
boolean same = c1.equals(c2); // byte-exact comparison after normalization

What are common mistakes?

The most common mistake is comparing JSON strings directly in tests, which breaks constantly when the serializer reorders fields or changes whitespace. The second mistake is ignoring the numeric type difference between 50000 and 50000.00.

JAVA
// Wrong – brittle raw string comparison in tests:
assertEquals(expectedJson, actualJson); // breaks on key reorder or whitespace

// Correct – tree comparison or JSONAssert:
JSONAssert.assertEquals(expectedJson, actualJson, JSONCompareMode.LENIENT);

What should a senior engineer know?

A senior engineer decides what "equal" means for the use case. In a response regression test, lenient order-independent equality is right. In a cryptographic signature check, byte-exact canonical JSON matters. In a reconciliation job, numeric-aware tree comparison prevents false alarms from formatting differences between systems. The comparison strategy follows from whether you care about meaning or exact representation.

Visual Overview

DIAGRAM
  Comparison Approaches:

  Raw String:       "{ amount:50000, account:SAV2001 }"
                    ≠
                    "{ account:SAV2001, amount:50000 }"    ← FAILS (key order)

  Tree (JsonNode):  parse → compare trees → key-order independent
                    ✓ structural equality

  JSONAssert:       LENIENT → ignores extras + array order
                    STRICT  → exact fields required

  zjsonpatch:       computes RFC 6902 diff
                    [{"op":"replace","path":"/balance","value":100000}]

  Canonical JSON:   sort keys + strip whitespace → byte-exact
                    for cryptographic signature verification

Key Takeaway

To compare two JSON strings, never compare the raw text. Raw-string comparison fails for three reasons that have nothing to do with the data: JSON objects are unordered, whitespace differs, and value formatting like 50000 versus 50000.0 differs as text while being the same number. Instead, parse both documents into Jackson trees with ObjectMapper.readTree and compare with JsonNode.equals, which performs deep structural comparison that is order-independent for objects and order-sensitive for arrays.

There is a numeric subtlety: by default Jackson treats an integer node and a decimal node as different types, so 50000 and 50000.00 are not equal under strict comparison. When you want numeric equality, pass a numeric-aware comparator. For tests, JSONAssert provides lenient mode to ignore extra fields and array order, and strict mode for exact match. For audit and change tracking, zjsonpatch computes a field-level diff.

At National Bank of India, regression tests had been comparing responses as raw strings and broke constantly when the serializer reordered fields, wasting developer time on false failures. Switching to tree comparison made the tests stable. And a reconciliation job raised a false alarm because a partner sent an amount formatted as 50000.00 while the stored copy had 50000, which the numeric-aware comparison correctly treated as equal.

The judgement is deciding what "equal" means for the situation. For regression tests, lenient equality. For cryptographic signatures, canonical byte-exact comparison. For reconciliation, numeric-aware tree comparison.

Remember one sentence: parse first, then compare the trees, never the strings, because JSON equality is about meaning, not formatting.

---

Share
Use ← → arrow keys to navigate