skip to content

In a Karate feature file `cat.kittens` holds two objects with ids 23 and 42. Why does `* match cat.kittens[*].id == 23` fail, and how do you assert on just the first id?

level: middleimportance: must knowfreq 70%

answer

  1. Star, dots and filters behave alike
  2. Query, not lookup
  3. One hit still arrives wrapped
  4. One prefix form collapses the result
  5. Index form exists only on get

basics

~20 s

The wildcard makes the path indefinite, so the left side is the array [23, 42] and never the scalar 23. Match the array, or collapse it in a def or on the right of the match with get[0] cat.kittens[*].id, which returns the first element.

solid answer

~60 s

The moment a path contains a wildcard `[*]`, a recursive descent `..`, or a filter `[?(...)]`, it stops being a lookup and becomes a query — JsonPath hands back a **list**, even when the list holds one item. So `cat.kittens[*].id` is `[23, 42]` and comparing it to `23` fails on type. You have three honest fixes: assert the whole selection (`match cat.kittens[*].id == [23, 42]`), assert membership (`match cat.kittens[*].id contains 23`), or collapse the list with the index form of `get` — `match cat.kittens[0].id == 23` if you really want position zero of the array, or `def first = get[0] cat.kittens[*].id` when the wildcard or filter is what produced the list. `get[N]` is the only form that indexes: the `$cat.kittens[*].id` short-cut has no index variant. It is an expression prefix, so it belongs in a `def` or on the **right** of the `match` (`match 23 == get[0] cat.kittens[*].id`) — at the head of a left side Karate 1.x splits it into the variable `get` plus a path and the step dies with `ReferenceError: get is not defined`.

code

gherkin · 25 lines
gherkin
* def cat =
  """
  {
    name: 'Billie',
    kittens: [
      { id: 23, name: 'Bob' },
      { id: 42, name: 'Wild' }
    ]
  }
  """

# the wildcard makes the left side a list
* match cat.kittens[*].id == [23, 42]
* match cat.kittens[*].name == ['Bob', 'Wild']

# membership, when you do not want to pin the whole array
* match cat.kittens[*].id contains 23

# collapse the list with the index form of get
* def actual = 23
* match actual == get[0] cat.kittens[*].id

# a filter is indefinite too, so it needs the same treatment
* def bob = get[0] cat.kittens[?(@.id == 23)]
* match bob.name == 'Bob'

go deeper

for a junior

Recall the rule as a sentence: a star, a filter or a double dot always hands you an array. Compare the array, or take an element out of it first.

for a middle

Explain definite versus indefinite paths, and show both fixes — the whole-array comparison and the index form of get — plus why the dollar short-cut cannot index.

for a senior

Watch for suites that reached for membership only to silence a type mismatch; that swap weakens the assertion and hides items added later. Push for the array comparison where the payload is stable.

for a principal

Decide as a standard whether selections are pinned whole or by membership, because the two choices give a suite very different sensitivity to payload growth and very different churn on every schema change.

## Indefinite paths return lists JsonPath distinguishes a **definite** path, which can only address one node, from an **indefinite** path, which addresses a set. Three things make a path indefinite: - a wildcard — `cat.kittens[*].id` - a recursive descent — `response..username` - a filter — `cat.kittens[?(@.id == 23)]` An indefinite path always evaluates to a **list**, regardless of how many nodes it happened to hit. One match gives you a one-element list, not the element. This is the single most common surprise for someone new to Karate's `match`, because the step reads like a field access: ```gherkin * def cat = { name: 'Billie', kittens: [{ id: 23, name: 'Bob' }, { id: 42, name: 'Wild' }] } # FAILS - the left side is [23, 42], the right side is a number * match cat.kittens[*].id == 23 # passes - compare the selection to the selection you expected * match cat.kittens[*].id == [23, 42] ``` Karate's `match` is a deep, order-sensitive structural comparison, so `[23, 42]` and `23` are not comparable values and the step fails with a type mismatch rather than quietly coercing. ## Three ways to say what you meant 1. **Assert the whole selection.** `match cat.kittens[*].id == [23, 42]` is usually the strongest assertion, because it pins the count as well as the values. Prefer it when the payload is small and stable. 2. **Assert membership.** `match cat.kittens[*].id contains 23` says "23 is among the ids" without caring about the rest. Useful when the array grows over time. 3. **Collapse the list to one element.** This is what the index form of `get` is for. ## `get[N]` collapses a list The `get` prefix evaluates a JsonPath against a named variable, and `get[N]` additionally returns element *N* of a list-shaped result: ```gherkin * def actual = 23 # instead of two steps * def kitnums = get cat.kittens[*].id * match actual == kitnums[0] # one step * match actual == get[0] cat.kittens[*].id ``` Note which side `get[0]` is on. It prefixes an *expression*, so Karate 1.x resolves it after `def ... =` and on the **right** of a `match` — never at the head of the left side, where `get[0] cat.kittens[*].id` is split into the variable `get` plus the path `$[0] cat.kittens[*].id` and the step fails with `ReferenceError: get is not defined`. It earns its keep with **filters**, where the array is an artefact of the query and you only ever wanted one row: ```gherkin * def bob = get[0] cat.kittens[?(@.id == 23)] * match bob.name == 'Bob' ``` Without the `[0]`, `bob` would be a one-element array and `bob.name` would be undefined. ## What indexes and what does not | Form | Applies a JsonPath | Takes an index | |---|---|---| | `get cat.kittens[*].id` | yes | no | | `get[0] cat.kittens[*].id` | yes | yes | | `$cat.kittens[*].id` | yes | **no** | | `cat.kittens[*].id` on the left of `match` | yes | no | | `get[0] cat.kittens[*].id` on the left of `match` | no — the step errors | no | The `$variable` short-cut is otherwise interchangeable with `get`, but the index is a convenience of the `get` spelling only. If you are reaching for `$` and an index, use `get[N]` instead. Also worth separating in your head: `cat.kittens[0].id` and `get[0] cat.kittens[*].id` are **not** the same expression. The first indexes the `kittens` array *inside* the path and stays definite, so it evaluates straight to `23`. The second runs a wildcard query and then takes the first element of the result set. They agree here, and they stop agreeing the moment the path is a filter or a recursive descent, where there is no positional index to write inside the path. ## The failure this prevents The reason interviewers ask is that the wrong mental model produces assertions that look right and are weak. Someone who believes a wildcard yields a scalar writes `match response.items[*].status == 'ACTIVE'`, watches it fail, and "fixes" it by switching to `contains`. That now passes if *any* item is active, which is a much weaker check than they think they wrote — and it keeps passing when a second, inactive item appears. Knowing that the left side is a list makes you choose deliberately between the whole-array comparison and membership. ## Reading a failure message When the shapes disagree, Karate reports a data-type mismatch and prints both sides, which is usually enough to spot the problem on the first run — the actual side arrives in brackets and the expected side does not. Train yourself to read that bracket as the signal it is: it means the path you wrote was a query. From there the decision is not "how do I make this pass" but "did I want one value or a set", and the answer determines which of the three fixes is honest. A wildcard you did not intend usually means the path should have been definite in the first place — `response.items[0].id` rather than `response.items[*].id` — and tightening the path beats loosening the assertion every time.

  • Is `cat.kittens[0].id` the same as `get[0] cat.kittens[*].id`?
    Not the same expression, though they agree in this example. `cat.kittens[0].id` indexes the array *inside* the path, so the path stays definite and evaluates straight to `23`. `get[0] cat.kittens[*].id` runs an indefinite query and then takes the first element of the result set. Only the second form is available when the query is a filter or a recursive descent, because there is no position to write inside the path.
  • Can you use the index form with the `$` short-cut, as in `$[0]cat.kittens[*].id`?
    No. The index is a convenience of the `get` spelling only — `$cat.kittens[*].id` applies the path but has no index variant. If you need both a path on a named variable and the first element, write `get[0] cat.kittens[*].id`.
  • How do you assert against the last element of an array?
    Use a JsonPath slice and collapse it: `get[0] list[-1:]` selects the final element, because `[-1:]` is an indefinite slice that returns a one-element list. In plain JavaScript the equivalent is `list[list.length - 1]`, which you can put in an ordinary `def`.

A wildcard is a query, not a lookup. SELECT id FROM kittens hands you a result set even when it holds one row — you still have to take the row out of it before comparing it to a number.

saying these in an interview costs you the question

  • Thinks a wildcard returns a scalar when one node matches
  • Switches to contains just to make the failure go away
  • Believes the dollar short-cut also takes an index
  • Writes get[0] at the head of a match left side
  • Confuses indexing inside the path with indexing the result
  • Assumes a filter that hits one row returns that row