In a Karate feature file, what do the expected values `'#[]'`, `'#[3]'` and `'#[] catSchema'` each assert about a JSON array in the response?
answer
- brackets after the hash
- inside the brackets versus after them
- the trailing token is evaluated
- define the shape once, reference it
- equivalent to match each
basics
~20 s'#[]' asserts the value is an array. '#[3]' adds that its length is 3. '#[] catSchema' evaluates catSchema as an expression and matches every element against the result, which is the same as writing match each.
solid answer
~50 sSquare brackets after the hash turn a marker into the array shortcut. `'#[]'` asserts only that the value is a present, non-null array. Anything inside the brackets is a length assertion, so `'#[3]'` requires exactly three elements and `'#[$.count]'` requires as many elements as the root payload's `count` field. Whatever follows the closing bracket is applied to **every element**: a second marker, as in `'#[3] #string'` (three elements, all strings), or a bare expression, as in `'#[] catSchema'`, which is evaluated — a variable, a property path, a function call — and matched against each element in turn. That last form is the schema-reuse idiom: `def` an object of markers once, then reference it wherever that shape appears. `'#[] catSchema'` is exactly equivalent to `match each ... == catSchema`, but it composes, so it can sit inside a larger expected payload where a separate `match each` step could not.
code
gherkin · 23 lines* def foo = ['bar', 'baz']
# should be an array
* match foo == '#[]'
# should be an array of size 2
* match foo == '#[2]'
# should be an array of strings with size 2
* match foo == '#[2] #string'
# reuse a defined shape for every element
* def catSchema = { id: '#uuid', name: '#string', age: '#number' }
* match response.cats == '#[] catSchema'
# same, but the array itself may be null
* match response.strays == '##[] catSchema'
# careful: on a path the ## does NOT cover a missing key - if the payload has no
# 'strays' at all, the step above fails with 'actual is not an array'
# inside an expected object it does, because the map walk skips a '##' value
* match response contains { strays: '##[] catSchema' }go deeper
Start with the two everyday forms: '#[]' for 'this is an array' and '#[3]' for a known length. Both read naturally on the right-hand side of a match.
Explain the two branches after the closing bracket — another marker applied per element, or an expression evaluated to a per-element schema — and why that is the same as match each.
Watch for '#[]' used where a length or an element shape was meant: it passes on an empty array, so an endpoint that quietly returns nothing still looks green.
Decide where shared marker objects live and who owns them, since a reused shape referenced from many features becomes a contract that changes ripple through.
## One marker, three jobs The array shortcut is a single marker string with up to three parts, and each part is optional: ``` # [ length ] per-element expectation ``` - `'#[]'` — the value must be an **array**, present and not `null`. Nothing else is checked. - `'#[3]'` — an array whose length is exactly 3. - `'#[] catSchema'` — an array, each element of which matches `catSchema`. - `'#[3] #string'` — an array of exactly three strings. - `'##[] catSchema'` — the same as the third form, except the array itself may be `null` (and, when the marker sits as a value inside an expected object, the key may be missing). ## Inside the brackets: length The bracket contents are evaluated as an expression against the array's length, so it is not limited to a literal. `'#[3]'` is the common case, and `'#[$.count]'` cross-checks one part of a payload against another — `$` is bound to the **root of the actual payload**, so this asserts the array has as many elements as a sibling `count` field claims: ```gherkin * def response = { count: 2, odds: [ { id: 1 }, { id: 2 } ] } * match response contains { odds: '#[$.count]' } ``` Empty brackets skip the length check entirely, which is why `'#[]'` is the idiomatic "is an array". A failing length reports the value it saw: `actual array length is 4`. ## After the brackets: the per-element expectation Whatever follows the closing bracket, trimmed, is applied to every element. The engine branches on its first character: 1. **It starts with `#`** — it is another marker, and each element is matched against it. `'#[] #string'`, `'#[2] #number'`, `'#[] #uuid'`. 2. **It does not start with `#`** — it is a **schema reference**: the text is evaluated as an expression and the result is matched against every element. That is where reuse comes from. The reference is an ordinary expression, not a special name, so a variable (`'#[] catSchema'`), a property path (`'#[] schemas.cat'`) or a call all work. The value it produces is normally an object of markers built with `def`. ## The reuse idiom Define the shape once and use it everywhere the shape appears, including nested inside another shape: ```gherkin * def barTwo = { title: '#string' } * def bar = { barOne: '#string', barTwos: '#[] barTwo', barThrees: '##[] barTwo' } * match response.foo.bars == '#[] bar' ``` `bar` is a plain variable holding a JSON object whose values happen to be marker strings, and `barTwos` reuses `barTwo` for each element of a nested array. Nothing is registered, compiled or loaded as a schema document — the composition is just variables referring to variables, resolved when the match runs. ## Single hash versus double hash The two hashes control different things, and it is worth being deliberate about which you mean: | expected value | value is `null` | key missing (map form) | path missing (`response.cats`) | array present | |---|---|---|---|---| | `'#[] catSchema'` | fail | fail | fail | elements checked | | `'##[] catSchema'` | **pass** | **pass** | **fail** — `actual is not an array` | elements checked | `'##[]'` makes the **array** optional, but "optional" here means `null` — plus, in the map form, a missing key, because the map walk skips any expected value that starts with `##` before it descends. It does **not** cover a missing **path**. `match response.cats == '##[] catSchema'` on a payload with no `cats` sees the actual degrade to the string `#notpresent`, which is not `null`, so the early exit for optional values never fires; execution reaches the bracket branch, whose first act is to ask whether the actual is a list, and the step fails with `actual is not an array`. The plain validators are more forgiving — that branch tests for not-present as well as `null`, which is why `'##string'` passes on a missing path where `'##[] catSchema'` does not. What `##` never loosens is the elements: once an array is actually there, every element is still matched against the schema. ## Why this exists at all `'#[] catSchema'` and `match each ... == catSchema` assert the same thing, and for a top-level array either will do. The marker form earns its place because it is a **value**, so it composes: - it can sit inside a larger expected payload, checking a nested array in the same step as everything around it, where a separate `match each` step cannot; - it can be nested to any depth, one shortcut per array; - it keeps the whole assertion in a single step, which is the property that makes a full-payload comparison worth writing in the first place. The practical shape of a suite that uses this well is a handful of `def`ed marker objects near the top of a feature or in a shared file, referenced by name from the assertions below — one place to change when the payload changes, and one name to read when the assertion is reviewed. ## Order of evaluation, and what a failure tells you The parts are checked in the order they are written, which matters when you are reading a failure: 1. **Is it an array at all?** A `null`, an object, or a path that resolved to nothing fails immediately with `actual is not an array`, and nothing after the brackets is attempted. 2. **Length.** The bracket contents are evaluated next; a mismatch reports what was actually there, `actual array length is 2`, so you do not have to print the payload to find out. 3. **Per element.** Only then is the trailing marker or schema applied, element by element, and the report descends into the failing index rather than dumping the whole array. So a failing `'#[3] #string'` on a two-element array tells you about the length and stops. Fix the length expectation and the run tells you about the elements. That staging is why the shortcut is usually easier to debug than an equivalent hand-written loop. ## A caution on the empty case `'#[]'` on its own passes for an empty array — it asserts the type and nothing else. An endpoint that starts returning `[]` instead of results therefore stays green under `'#[]'`, and this is the single most common way the shortcut is used more loosely than intended. If a non-empty result is part of what you are asserting, say so with a length expression rather than relying on the bare form.
- In a Karate feature file, `response.items` is `null`. How do `'#[] itemSchema'` and `'##[] itemSchema'` differ there?`'#[] itemSchema'` fails: the single hash requires a present, non-null array, and a `null` is not one. `'##[] itemSchema'` passes on `null`, because the `##` prefix short-circuits to a pass before the bracket form is ever parsed. An **absent** value is a different story, and it depends on where the marker sits: inside an expected object — `match response == { items: '##[] itemSchema' }` — the map walk skips any expected value starting with `##`, so a missing key passes; but against a path — `match response.items == '##[] itemSchema'` — the actual degrades to `#notpresent`, which is not `null`, so the bracket branch runs its "is this an array?" check and the step fails with `actual is not an array`. (The plain validators are more forgiving here: `'##string'` passes on an absent path, because that branch tests `isNotPresent()` as well as `isNull()`.) Neither form changes what happens once an array is genuinely present: every element is still matched against `itemSchema`.
- In a Karate feature file, does the text after `'#[] '` have to be a variable name?No. It is evaluated as an expression, so a property path such as `'#[] schemas.cat'` or a call works just as well; a variable is simply the common case. The one branch that is not an expression is a value starting with `#`, which is read as another marker and applied per element instead — `'#[] #string'` asserts an array of strings rather than looking up something called `#string`.
saying these in an interview costs you the question
- Thinks '#[]' also asserts the array is non-empty
- Reads '#[] catSchema' as matching the array against catSchema as a whole
- Says the brackets must contain a literal number
- Believes '##[]' also lets individual elements be null
- Thinks '##[] schema' lets a missing path pass the way '##string' does
- Assumes the schema reference must be a file or registered name
- Says a nested array needs its own separate match each step