In a Karate feature file, `* def original = { a: 1, b: 2, c: 3, d: { a: 1, b: 2 } }`. Why does `match original contains { a: 1, d: { b: 2 } }` fail, and which operator makes it pass?
answer
- leniency applies at one level only
- the child comparison falls back to equality
- a nested chunk is compared whole
- one extra word fixes the whole walk
- no negated deep form exists
basics
~20 scontains is lenient only at the level it is applied to. Descending into a nested object or array, the child comparison reverts to equality, so d must equal the expected literal exactly and fails. Use contains deep.
solid answer
~40 s`contains` relaxes the comparison at exactly one level — the one the operator is written against. Walking `original`, Karate finds the key `d`, and because the operator is plain `contains`, the child comparison for that nested value is **equality**, not containment. So `{ a: 1, b: 2 }` is compared with `==` against `{ b: 2 }`, the extra key `a` makes it fail, and the step reports a failure under `d`. `match original contains deep { a: 1, d: { b: 2 } }` fixes it: `contains deep` propagates containment semantics to every nested map, list and XML node, while scalars are still compared with `==`. There is no `!contains deep` — the negated deep form does not exist.
code
gherkin · 10 lines* def original = { a: 1, b: 2, c: 3, d: { a: 1, b: 2 } }
# fails: 'd' is descended into with '==' , and { a: 1, b: 2 } is not { b: 2 }
# * match original contains { a: 1, d: { b: 2 } }
# passes: containment is carried down into every nested map and list
* match original contains deep { a: 1, d: { b: 2 } }
* def nested = { a: 1, arr: [ { b: 2, c: 3 }, { b: 3, c: 4 } ] }
* match nested contains deep { a: 1, arr: [ { b: 2 }, { c: 4 } ] }go deeper
Remember the one-line rule: contains relaxes only the level it is written against, and contains deep is the version that keeps relaxing further down. Recognising which one a step needs is enough here.
Be able to trace the walk: which key is descended into, which operator the child comparison uses, and why the reported failure sits on a nested path rather than the root.
Spot the drift where an expected object grows a nested literal and quietly tightens the contract from subset to exact. That is the change that turns a stable assertion into a flaky one.
Decide how much of a payload a suite is allowed to pin at all, and make the deep-versus-shallow choice explicit in review guidance rather than leaving it to whoever last edited the expected literal.
## One level of leniency, not all of them The rule worth memorising is short: **`contains` is lenient at the level it is applied to, and only at that level.** Internally the comparison chooses a child operator for every value it descends into, and for plain `contains` that child operator is always equality. `contains deep` is the variant that keeps handing containment down. Walk the example step by step: 1. The operator is `contains`, so the expected map `{ a: 1, d: { b: 2 } }` drives the walk. 2. Key `a` is found on the actual side; its value is a scalar, so it is compared with `==`. `1 == 1` passes. 3. Key `d` is found; its actual value is a **nested object**. Because the operator is not deep, the child comparison is equality. 4. `{ a: 1, b: 2 } == { b: 2 }` fails — under `==` the expected literal must describe the whole object, and the actual object carries one more key. Nothing about step 4 is special-cased for nesting; it is simply what `==` means, applied one level down. ## `contains deep` `contains deep` changes exactly step 3. Whenever the value being descended into is a map, a list or XML, the child comparison stays in the containment family instead of collapsing to equality. Scalars are still compared with `==`, because there is no such thing as a partial number. ```gherkin Scenario: recurse nested json * def original = { a: 1, b: 2, c: 3, d: { a: 1, b: 2 } } * def expected = { a: 1, c: 3, d: { b: 2 } } * match original contains deep expected Scenario: recurse nested array * def original = { a: 1, arr: [ { b: 2, c: 3 }, { b: 3, c: 4 } ] } * def expected = { a: 1, arr: [ { b: 2 }, { c: 4 } ] } * match original contains deep expected ``` The second scenario shows why this matters for real payloads: the elements of `arr` are matched as partial objects, and because array containment is order-free, `{ c: 4 }` finds the second element even though it was written second and the first element does not contain it. ## The family at a glance | operator | top level | nested maps and lists | |---|---|---| | `==` | must describe the whole value | must describe the whole value | | `contains` | subset | full equality | | `contains deep` | subset | subset, all the way down | | `contains only deep` | same elements, any order | same elements, any order, at every depth | `contains only deep` is the mirror image and worth knowing for one specific job: it behaves like `==` except that array order is ignored **at every depth**, so `{ foo: ['a', 'b'] }` matches `contains only deep { foo: ['b', 'a'] }` while still failing if a key is missing or extra. ## Two denials that catch people out - **There is no `!contains deep`.** The negated deep type simply does not exist in the operator set, and the documentation says as much. If you need to assert that a nested chunk is absent, reach for a marker such as `#notpresent` in an equality match, or assert on the specific path. - **`each` does not combine with every deep spelling.** `match each x contains deep y` is a real operator, but the only-deep and any-deep spellings have no `each` counterpart, so do not assume the grid is complete just because the two words each exist. ## Why this bites in practice Response payloads nest. A typical assertion starts life as `match response contains { status: 'OK' }`, passes for months, and then someone adds `data: { id: '#number' }` to the expected object because the payload grew. That new line silently changes the contract for `data` from "contains" to "must be exactly this", and the step starts failing the day the service adds a field inside `data` — which is the opposite of what the author of the `contains` step wanted. The same trap applies to XML, which is converted to a map before comparison, so a partial subtree under a nested element needs the deep form too. The tell in a failure report is that the reported path points at a **nested** key while the message reads like an equality failure. When you see that under a `contains` step, the fix is almost always `contains deep` rather than more expected keys.
- Does `contains deep` also relax scalar values?No. Containment is handed down only when the value being descended into is a map, a list or XML; scalars are always compared with equality, because a partial number or boolean has no meaning. A string value nested inside a deep match is compared for equality too, not as a substring.
- How does `contains only deep` differ from `contains deep`?`contains deep` allows extra keys and extra array elements at every depth. `contains only deep` behaves like `==` — no extra keys, arrays of equal length — except that array order is ignored at every depth. Use it when the payload is complete but the service does not promise a stable ordering.
- Can you write `!contains deep`?No — the negated deep operator does not exist. `!contains` is available and negates the plain, single-level containment result. To assert that something nested is absent, use an equality match with a `#notpresent` marker at the path in question, or assert directly on that path.
Plain contains is a doorman with a guest list: he confirms the names you care about and ignores everyone else — but the moment he is handed a sealed envelope (a nested object) he insists it be identical to the one on file, not merely that it hold what you asked for. contains deep opens every envelope the same lenient way.
saying these in an interview costs you the question
- Says contains recurses into nested objects by default
- Thinks the failure means the nested key is missing
- Adds more expected keys instead of switching to contains deep
- Assumes !contains deep exists as an operator
- Believes contains deep also relaxes scalar comparisons
- Claims contains deep ignores extra array elements only at the top