skip to content

Deep Equality

A single == compares an entire payload, ignoring key order in objects but not element order in arrays. Interviewers ask because the strict compare is the assertion that breaks first.

on this pageshow

explore

questions

5

In a Karate feature file, what does the step `* match order == { id: 1, name: 'Billie' }` assert, and what happens if `order` also carries a `role` key?

level: juniorimportance: must knowfreq 84%

answer

  1. one step asserts a whole body
  2. the comparison recurses into nesting
  3. strict in both directions
  4. surplus keys are not tolerated
  5. objects by key, arrays by index

basics

~20 s

Karate's match == compares the entire payload in one recursive step: every key and value must correspond exactly. An extra role key fails the step, because == is a strict whole-payload comparison, not a subset check.

solid answer

~50 s

`match` is Karate's built-in assertion — there is no step-definition class behind it and no matcher library to import. The step evaluates the left operand as a variable (optionally followed by a JsonPath or XPath), parses the right operand as a Karate data literal, then walks both sides together: objects key by key, arrays element by element, scalars by value. The comparison is **strict in both directions**, so the actual payload may not carry keys the expected side never named. An `order` of `{ id: 1, name: 'Billie', role: 'admin' }` therefore fails with `actual has 1 more key(s) than expected`, and Karate prints the surplus. Whitespace and key order are irrelevant, and numbers compare by numeric value rather than by Java type. `==` is the assertion that pins a payload; `contains` is the separate operator for subset checks.

code

gherkin · 9 lines
gherkin
Feature: deep equality

  Scenario: one step pins the whole payload
    * def order = { id: 1, name: 'Billie' }
    * match order == { name: 'Billie', id: 1 }

  Scenario: a surplus key fails the strict compare
    * def bigger = { id: 1, name: 'Billie', role: 'admin' }
    * match bigger != { id: 1, name: 'Billie' }

go deeper

for a junior

Recall that one match ... == step asserts the whole payload and that it is strict: a key the expected side never named still fails the step.

for a middle

Explain the recursion — objects compared by key lookup, arrays by index, scalars by value — and name the three distinct object failures: missing key, wrong value, surplus key.

for a senior

Know what the strictness buys you in production: a whole-payload == is the assertion that catches a field a service silently added, which a targeted check would sail past.

for a principal

Weigh the cost of strictness across a suite — every additive change to a shared contract touches every == assertion, so decide deliberately where that pressure is a feature and where it is churn.

## `match` is the assertion, not glue Karate has no step-definition registry and no glue path. A line like `* match order == { id: 1, name: 'Billie' }` is executed by the runtime itself: `match` is a first-class keyword and `==` is its comparison operator. There is no class to write, no matcher library to import, no fluent assertion API to chain. That one step is a complete assertion over an entire document. The step takes two operands. The left one names a variable and may be followed by a JsonPath or XPath, so `match order $.items[0].sku == 'PEN-1'` compares only that slice. The right one is evaluated as a Karate data literal, which is why the unquoted, JSON-ish `{ id: 1, name: 'Billie' }` becomes a real map before the comparison starts — quoting keys is optional. Karate then walks both sides together. ## Recursive, type by type `==` dispatches on the data type of the actual value and recurses into whatever it finds: | actual type | how `==` compares it | |---|---| | object / map | every expected key is looked up in the actual map, then compared recursively | | array / list | lengths are checked first, then element by element, by index | | string | exact string equality | | number | numeric value, so an `Integer` 1000 equals a `BigDecimal` 1000 | | boolean | value | | byte array | byte for byte, which is why `match responseBytes == read('test.pdf')` works | | XML node | converted to a map and walked exactly like JSON | Because the walk recurses, nesting depth costs you nothing. One step pins a five-level document as firmly as a flat one, and the failure report names the level that broke. ## Strict in both directions Three distinct things can go wrong when both sides are objects: 1. **A key the expected side names is absent from the actual payload** — the failure reads `actual does not contain key - 'name'`. 2. **A shared key holds a different value** — the parent line reports `match failed for name: 'name'` and a nested line names the exact path and the two values. 3. **The actual payload carries a key the expected side never named** — `actual has 1 more key(s) than expected`, followed by a printout of just the surplus. The third case is the one candidates get wrong. `==` is not a subset assertion. `{ id: 1, name: 'Billie', role: 'admin' }` fails against `{ id: 1, name: 'Billie' }` even though every key that *was* named matches. That strictness is exactly the point: a whole-payload `==` is the assertion that notices a field the service quietly started returning. ## What deliberately does not matter - **Whitespace and formatting** — the right side is parsed into data, never string-compared. - **Key order inside objects** — expected keys are looked up by name, so a reordered object still passes. - **The Java type of a number** — comparison is by numeric value, not by wrapper class. - **How the payload arrived** — the same engine compares a variable you built, an HTTP response body, and a file read from disk. ## The negated form `!=` is the same comparison, inverted: Karate runs the full recursive equality check and passes when that check fails. `match bigger != { id: 1, name: 'Billie' }` passes precisely because the surplus `role` key made `==` fail. The corollary is that `!=` only ever tells you the two payloads differ somewhere — never where. It is a weak assertion by construction, useful for pinning "this is definitely not the stale record" and little else. ## When the path resolves to nothing If the left operand carries a path that matches no node, the failure is `actual path does not exist` rather than a value mismatch. That distinction is worth recognising in a report: it means the shape of the document changed, not that a value drifted. ## The same engine from JavaScript `karate.match('order == { id: 1 }')` and `karate.match(actual, expected)` both run the comparison and hand back a result object carrying a `pass` flag instead of failing the step. That is the escape hatch for the rare case where you need to branch on the outcome rather than assert it. Prefer the string form: it is dispatched through the very evaluator the `match` keyword uses, so it behaves identically. The two-argument form is not quite the keyword. On Karate 2.x it pre-parses an operand that looks like JSON or XML before comparing — something the keyword never does — so `karate.match('{ "id": 1 }', { id: 1 })` reports a pass while the equivalent `* def raw = '{ "id": 1 }'` and `* match raw == { id: 1 }` fail on `(STRING:MAP)`. On Karate 1.5.2 the two-argument form reaches the same engine entry point the keyword does, and the two agree.

  • In Karate, does `match total == 1000` still pass when the payload holds the value as a BigDecimal rather than an int?
    Yes. Karate compares numbers by numeric value, not by Java wrapper class, so an `Integer` 1000, a `Double` 1000.0 and a `BigDecimal` 1000 are all equal to each other. This matters because a JSON parser's choice of numeric type is an implementation detail you should not have to assert around.
  • In Karate, what does the failure say when the expected object names a key the actual payload does not have?
    You get `actual does not contain key - 'name'` on the line for the enclosing object, together with the path of that object. That is a different message from a value mismatch, which reads `match failed for name: 'name'` and adds a nested line naming the leaf path and printing both values.
  • In a Karate feature file, can the left side of `match` be something other than a bare variable name?
    Yes — the left operand may be a variable followed by a JsonPath or XPath, so `match order $.items[0].sku == 'PEN-1'` compares only that slice with the same recursive engine. If the path resolves to nothing, the failure is `actual path does not exist` rather than a value mismatch.

saying these in an interview costs you the question

  • Says == is a subset check that ignores extra keys
  • Thinks a match step needs a step definition or glue class
  • Believes key order in a JSON object must match
  • Assumes == only compares the top level, not nested objects
  • Claims a JSON-looking string is auto-parsed before comparison
  • Thinks != tells you which field differed
open as a page

In Karate's `match actual == expected`, does the order of keys in a JSON object matter, and does the order of elements in a JSON array matter?

level: middleimportance: must knowfreq 72%

basics

~20 s

Key order in a JSON object is irrelevant, because Karate looks each expected key up in the actual map by name. Element order in a JSON array is significant: lists are compared index by index after a length check.

open as a page

In a Karate feature file, `* match each items == { active: true }` runs when `items` is an empty array. Does the step pass or fail, and how do you change that?

level: middleimportance: should knowfreq 46%

basics

~10 s

The step fails. Karate's match each rejects an empty array by default and reports 'match each failed, empty array / list'. Adding the step configure matchEachEmptyAllowed = true makes an empty list pass vacuously.

open as a page

In a Karate feature file, `* match order == { id: 1 }` fails with `data types don't match`. What causes that, and what does `* match order != { id: 1 }` do in the same situation?

level: middleimportance: should knowfreq 40%

basics

~20 s

Karate compares the two sides' data types before their contents, so a string on the left and an object on the right can never be equal and the step reports data types don't match. The != form passes for exactly that reason.

open as a page

A Karate step `* match order == expected` fails on a deeply nested JSON body. What does Karate's `match failed:` report give you beyond "the two payloads differ"?

level: seniorimportance: should knowfreq 52%

basics

~20 s

Karate prints the match type, then one indented block per level of the failure path: the JsonPath, the reason, the two data types, and the actual and expected values at that node, ending at the exact leaf that differed.

open as a page