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?
answer
- Star, dots and filters behave alike
- Query, not lookup
- One hit still arrives wrapped
- One prefix form collapses the result
- Index form exists only on get
basics
~20 sThe 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 sThe 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* 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
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.
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.
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.
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