In a Karate feature file, the response carries a server-generated `id` and a `createdAt` timestamp that differ on every run. How do you still assert the entire payload in a single `match` step?
answer
- assert the shape, not the value
- a string inside the expected payload
- the hash-prefixed strings
- '#uuid', '#number', '#notnull'
- eleven validators plus #regex
basics
~20 sPut marker strings where the unpredictable values would go: match response == { id: '#uuid', createdAt: '#string', name: 'Billie' }. A marker asserts the value's type or shape instead of its content, so one step still checks every key.
solid answer
~50 sKarate's `match` compares a whole expected payload in one step, and `==` is exact in both directions — a response key the expected object omits fails with `actual has 1 more key(s) than expected`. To keep that single-step check while values vary, write a **marker string** where the literal would sit: `* match response == { id: '#uuid', name: '#string', hits: '#number', updated: '#notnull' }`. Eleven validator names are built in — `#array`, `#boolean`, `#ignore`, `#notnull`, `#null`, `#number`, `#object`, `#present`, `#notpresent`, `#string`, `#uuid` — plus `#regex <pattern>`, which compiles the rest of the string as a regular expression. Markers work at any depth, on array elements, as the text of an XML element, and on a primitive with a path on the left (`match response.id == '#uuid'`). No matcher class, custom assertion or step definition is involved: the marker is just a string in the expected value.
code
gherkin · 16 lines* def response = { id: 'a9f7a56b-8d5c-455c-9d13-808461d17b91', name: 'Billie', hits: 4, tags: ['x'], meta: { ok: true } }
# one step, every key checked, nothing hard-coded that varies
* match response ==
"""
{
id: '#uuid',
name: '#string',
hits: '#number',
tags: '#array',
meta: '#object'
}
"""
# markers work on a primitive too, with a path on the left
* match response.id == '#uuid'go deeper
Memorise the everyday five — '#string', '#number', '#boolean', '#notnull' and '#uuid' — and the habit of putting them in the expected payload rather than deleting the volatile key.
Be able to explain that '==' fails on surplus keys too, and that the marker is interpreted during the payload walk rather than by any matcher class or glue code.
Know which markers tolerate an absent key, and why swapping '#string' for '#ignore' to quiet a flaky field quietly removes the only check on that field.
Weigh how much of a payload should be pinned literally versus by shape: markers make a full-payload assertion cheap, which is exactly what makes over-loosening them easy to miss in review.
## Why the expected payload carries markers Karate's `match` keyword compares an actual value against an expected one in a single step, and for `==` that comparison is exact in **both** directions. If the response object carries a key the expected object does not, the step fails with `actual has 1 more key(s) than expected` and prints the surplus keys; if the expected object names a key the response lacks, it fails with `actual does not contain key`. That strictness is the whole point — it is what stops a silently added, renamed or dropped field from slipping through a green test. It also means a payload containing a generated `id`, a timestamp or a signed token cannot simply be written out literally, because the literal differs on every run. A **marker string** resolves that without giving up the one-step check. Anywhere a literal value would sit in the expected payload you write a string beginning with `#`. The match engine sees the `#`, looks the rest of the name up in a table of built-in validators, and runs that validator against the actual value instead of comparing content. ```gherkin * def cat = { name: 'Billie', type: 'LOL', id: 'a9f7a56b-8d5c-455c-9d13-808461d17b91' } * match cat == { name: '#ignore', type: '#regex [A-Z]{3}', id: '#uuid' } ``` ## The built-in validators Eleven validator names ship in the engine, plus the `#regex` prefix, which is compiled per use rather than looked up: | marker | passes when the actual value is | |---|---| | `#string` | a string, and the key is present | | `#number` | a number | | `#boolean` | `true` or `false` | | `#array` | a JSON array or list | | `#object` | a JSON object or map | | `#uuid` | a string that parses as a UUID | | `#null` | `null`, with the key present | | `#notnull` | present and not `null` | | `#present` | present, of any type, `null` allowed | | `#notpresent` | absent from the payload altogether | | `#ignore` | anything at all, present or not | | `#regex STR` | a string the pattern `STR` matches end to end | ## Where a marker may appear - **At any depth.** A nested object is walked key by key, so `{ user: { id: '#uuid' } }` works exactly like the top level. - **Inside an array literal**, one marker per element position. - **On a primitive**, with a JsonPath on the left: `match response.id == '#uuid'` or `match responseStatus == '#number'`. - **In an expected XML payload**, as the element's text and *unquoted*: `match response == <root><a>#string</a></root>`. - **Mixed with literals in the same object** — the usual shape is a handful of markers among mostly literal values, which is what makes the assertion tight rather than vague. ## Reading a failure The report descends to the failing node and prints the JSON path, the validator's own message and both values, so you rarely have to add debug output: ``` $.b | regex match failed (STRING:STRING) 'foo' '#regex .{2}' ``` Here `.{2}` failed against `'foo'` because a `#regex` marker is a **whole-string** match, not a search — the pattern must consume the entire value. `'#regex .{3}'` or `'#regex fo.*'` would pass. ## Three details that catch people out 1. **Escaping needs a double backslash.** The expected payload is parsed as JSON/JS before the match engine sees it, so a literal dot is `'#regex a\\.dot'`, not `'#regex a\.dot'`. 2. **`#uuid` and `#regex` both demand a string first.** Each fails with `not a string` on a number or an object, so `'#uuid'` against a numeric id fails on the type before the format is ever considered. 3. **A missing key is not the same as a null one.** `'#string'` fails outright when the key is absent; only `'#ignore'`, `'#notpresent'` and a `##`-prefixed marker survive an absent key. ## What this replaces There is no matcher library, no assertion class and no step definition behind any of this. The marker is an ordinary string sitting in the expected value, interpreted during the payload walk. That is why a full-payload assertion stays one line, and why the same eleven names work identically in JSON, in XML and against a bare primitive. ## Choosing how tight to be The eleven names form a strictness ladder, and every step down it removes a check rather than changing one. For a generated identifier the rungs run: 1. `'#uuid'` — a string, and it parses as a UUID. 2. `'#string'` — a string, and the key is present. 3. `'#notnull'` — present, and not `null`. 4. `'#present'` — present, and that is all; `null` passes. 5. `'#ignore'` — nothing is checked, and even an absent key passes. Reach for the highest rung the value actually supports. The temptation is always downward, because a lower rung makes a red step go green, and the cost is invisible in the diff: swapping `'#uuid'` for `'#ignore'` to quiet one flaky field looks like a one-word change and removes the only assertion that field had. ## The marker is a value, not syntax Nothing about a marker is special to the parser. It is a string sitting in an ordinary JSON value, so a whole expected payload can be `def`ed as a variable, `read` in from a `.json` file, nested inside a larger expected payload, or passed between features like any other data. That is the property the rest of this vocabulary is built on — reuse across assertions is variable reuse, with no registry, no compilation step and nothing to keep in sync with the tests that use it.
- In a Karate feature file, what does `match response == { name: '#ignore' }` assert about the `name` key?Almost nothing about the value — `#ignore` passes whether `name` holds a string, a number, `null`, or is missing from the payload entirely. It is one of only three expected values that survive an absent key, alongside `#notpresent` and any marker starting with `##`. Its real job is the surrounding `==`: it lets you keep the exact whole-payload comparison on every other key while excusing one you cannot predict.
- In a Karate feature file, why does `match id == '#regex hello'` fail when `id` is `'hello1'`?Because a `#regex` marker runs a whole-string match, not a search: the pattern has to consume the entire value, and `hello` leaves the trailing `1` unconsumed. Use `'#regex hello[0-9]+'` or `'#regex hello.*'` to match the full string. If you genuinely want a substring test, that is a different assertion — the regex marker will never behave like one.
saying these in an interview costs you the question
- Says Karate validates the response against a JSON Schema document
- Thinks a marker only works at the top level, not inside nested objects
- Assumes '#string' also passes when the key is missing
- Believes markers need a custom matcher class or step definition
- Expects '#regex' to search inside the value rather than match all of it
- Uses '#uuid' on a numeric id and expects the format check to run