🧠 The Definitive Reference

Karate
API Assertions

// Every assertion type, every fuzzy marker, every match variant, every JS/Java interop pattern — with examples for each. The only Karate assertion reference you'll ever need.

match / assert / karate.expect()
JSON · XML · GraphQL · gRPC
Fuzzy Matching · Schema Validation
JavaScript · Java Interop
// Table of Contents
01 Assertion Overview 02 Assertion Decision Flow 03 match == & match != 04 match contains & Variants 05 match each 06 Fuzzy Matching Markers 07 Schema Validation 08 Self-Validation (#?) & Custom Logic 09 Response Variables & Status 10 Header, Cookie & Time Assertions 11 XML Assertions 12 JsonPath & Data Extraction 13 JavaScript Function Assertions 14 Java Interop Assertions 15 karate.expect() — Chai-Style BDD 16 Soft Assertions & Labels 17 Retry Until & Polling 18 GraphQL & Async Assertions 19 Data Manipulation for Assertions 20 Contains Shortcuts & Symbols 21 Real-World Assertion Recipes
01

Assertion Overview

Karate provides the most powerful built-in assertion engine of any API testing framework. No external libraries needed — no RestAssured, no Hamcrest, no AssertJ. Everything is built in.

Key Insight
The match keyword is Karate's primary assertion. Prefer it over assert — it gives better error messages showing expected vs actual values and supports JSON, XML, and text natively.
Three Assertion Approaches:
match Primary assertion. Supports exact, contains, each, fuzzy markers, and deep matching. Recommended
assert For numeric comparisons (> < >= <=) and boolean expressions only.
karate.expect() Chai-style BDD assertions for teams migrating from Postman/Mocha. v2+
Karate · Three Assertion Styles
Feature: Assertion styles overview

Scenario: Using match (preferred)
  Given url 'https://api.example.com/users/1'
  When method get
  Then status 200
  And match response.name == 'John Doe'
  And match response.id == 1
  And match response contains { role: 'admin' }

Scenario: Using assert (numeric only)
  * assert responseStatus == 200
  * assert response.age > 18
  * assert response.items.length >= 1

Scenario: Using karate.expect() (Chai-style)
  * karate.expect(response.name).to.equal('John')
  * karate.expect(response.age).to.be.above(18)
  * karate.expect(response).to.have.property('email')
02

Assertion Decision Flow

📨 Response Get API response (JSON / XML / text)
Status Then status 200
🔍 Exact? match == for full equality
📦 Partial? match contains for subset
🔬 Type? #string #number #uuid etc.
🛠 Custom? #? JS expression / Java util
🎯 Pass/Fail Clear error messages on failure
03

match == & match != — Equality

The foundation of all Karate assertions. match == performs intelligent equality that ignores key ordering and handles whitespace. match != asserts inequality.

match == · Exact Equality
# JSON object — key order doesn't matter
* def user = { id: 1, name: 'John' }
* match user == { id: 1, name: 'John' }
* match user == { name: 'John', id: 1 }

# Property-level match
* match user.id == 1
* match user.name == 'John'

# Arrays — order DOES matter
* def nums = [1, 2, 3]
* match nums == [1, 2, 3]

# Match against variables
* def expected = { id: 1, name: 'John' }
* match user == expected

# Strings
* def msg = 'Hello World'
* match msg == 'Hello World'

# Numbers
* match response.total == 42.5

# Booleans
* match response.active == true

# Null
* match response.deletedAt == null
match != · Not Equals
# Simple negative assertions
* def user = { id: 123, status: 'active' }

* match user != { id: 456 }
* match user.status != 'inactive'
* match user.id != 0

# Text not equals
* def message = 'Success'
* match message != 'Error'
* match message != ''
Best Practice
Prefer match == with fuzzy markers like #notnull or #notpresent over match != for complex negative validations. Reserve != for simple string/number comparisons.
match within · Range Check (v2)
# Assert value is within a range
* def temp = 36.8
* match temp within { low: 36.0, high: 37.5 }

# Assert value is OUTSIDE a range
* def score = 85
* match score !within { low: 0, high: 50 }
04

match contains & All Variants

Contains operations let you assert subsets without requiring exact equality. Six powerful variants cover every partial matching scenario.

📦 match contains
Subset matching — check specific fields exist without matching ALL fields.
# Object — only check specified keys
* match response contains { id: 1, name: 'Bret' }

# Array — check element exists
* def tags = ['admin', 'verified', 'premium']
* match tags contains 'admin'

# Multiple elements (order irrelevant)
* match tags contains ['admin', 'verified']
🚫 match !contains
Verify elements are absent from objects or arrays.
* def user = { id: 1, name: 'John' }
* match user !contains { deleted: true }
* match user !contains { id: 456 }

* def tags = ['admin', 'verified']
* match tags !contains 'suspended'
* match tags !contains [4, 5]
🔁 match contains only
All elements must be present in any order — no extras allowed.
* def nums = [1, 2, 3]
* match nums contains only [3, 2, 1]
* match nums contains only [2, 3, 1]

# This FAILS — missing element 3:
# * match nums contains only [1, 2]
🎲 match contains any
At least ONE element from the expected list must be present.
* def tags = ['admin', 'verified', 'premium']
* match tags contains any ['admin', 'superuser']

# Works for objects too
* def data = { a: 1, b: 'x' }
* match data contains any { b: 'x', c: true }
👁 match contains deep
Recursive subset matching for nested structures — checks at any depth.
* def data = { a: 1, b: 2, d: { x: 10, y: 20 } }
* def expected = { a: 1, d: { y: 20 } }
* match data contains deep expected

# With nested arrays
* def data2 = { arr: [{ b: 2, c: 3 }, { b: 4}] }
* match data2 contains deep { arr: [{ b: 2 }] }
🌱 match contains only deep
Like match == but array order doesn't matter at any depth.
* def resp = { foo: ['a', 'b', 'c'] }

# Order doesn't matter at any depth
* match resp contains only deep { foo: ['c', 'a', 'b'] }
Quick Decision Guide
contains = "has at least these" · contains only = "has exactly these, any order" · contains any = "has at least one of these" · contains deep = "has at least these, at any nesting level"
05

match each — Array Element Validation

match each applies a validation pattern to every element in an array. Combine with fuzzy markers and contains for maximum power.

match each · All Patterns
# Type-check every element
* match each response ==
  """
  {
    id: '#number',
    name: '#string',
    email: '#string',
    active: '#boolean'
  }
  """

# Each element contains subset
* match each response contains { id: '#number' }

# Each element with custom validation
* match each response == { id: '#number? _ > 0' }

# Simple array of primitives
* def tags = ['api', 'test', 'karate']
* match each tags == '#string'

# Each with custom JS expression
* def statusEnum = ['active', 'pending']
* match each response..status == '#? statusEnum.includes(_)'
Cross-Field & Deep Variants
# _$ references the parent object
* def orders =
  """
  [
    { items: 3, total: 30, pricePerItem: 10 },
    { items: 5, total: 25, pricePerItem: 5 }
  ]
  """

# Validate total == items * pricePerItem
* match each orders contains
  { total: '#? _ == _$.items * _$.pricePerItem' }

# match each contains deep
* def items =
  """
  [
    { a: 1, arr: [{ b: 2, c: 3 }] },
    { a: 1, arr: [{ b: 2, c: 3 }, { b: 4 }] }
  ]
  """
* match each items contains deep
  { a: 1, arr: [{ b: 2 }] }
SymbolMeaningContext
_Current value being validated ("self")Any #? expression
$JSON root documentCross-field validation
_$Parent object in iterationmatch each only
06

Fuzzy Matching Markers — Complete Reference

Validate data types and patterns without hardcoding exact values. These markers are what make Karate uniquely powerful — no other framework has this built in.

MarkerDescriptionExampleMatches
#ignoreSkip validation entirely for this fieldmatch resp == { id: '#ignore' }Any value
#nullValue must be null (key MUST exist)match resp == { v: '#null' }{ v: null }
#notnullValue must not be nullmatch resp == { v: '#notnull' }{ v: 'anything' }
#presentKey must exist (any value, even null)match resp == { v: '#present' }{ v: null } or { v: 1 }
#notpresentKey must NOT existmatch resp == { v: '#notpresent' }{ } (no key v)
#stringMust be a stringmatch resp == { name: '#string' }"John"
#numberMust be a numbermatch resp == { age: '#number' }25, 3.14
#booleanMust be true or falsematch resp == { ok: '#boolean' }true, false
#arrayMust be a JSON arraymatch resp == { items: '#array' }[1,2,3]
#objectMust be a JSON objectmatch resp == { addr: '#object' }{ street: '...' }
#uuidMust match UUID formatmatch resp == { id: '#uuid' }"550e8400-e29b..."
#regex STRMust match regular expressionmatch resp == { email: '#regex .+@.+' }"a@b.com"
#? EXPRCustom JS expression must return truematch resp == { age: '#? _ > 18' }Any number > 18
#[N]Array must have N elementsmatch resp == '#[10]'Array of length 10
#[] SCHEMAArray with optional element schemamatch resp == '#[] #string'Array of strings
#(EXPR)Embedded expression (substituted before match)match resp == { v: '#(expected)' }Value of expected variable
Optional Fields with ## Prefix
## · Optional / Nullable Fields
# Field can be missing OR match the type
* def user = { name: 'John', age: 30 }
* match user ==
  {
    name: '#string',
    age: '#number',
    bio: '##string',     # missing = OK
    phone: '##string'    # missing = OK
  }

# ##null matches both missing AND null
* def foo = { a: 1 }
* match foo == { a: 1, b: '##null' }

* def bar = { a: 1, b: null }
* match bar == { a: 1, b: '##null' }

# Works with any marker
# ##string, ##number, ##object, ##array, etc.
#null vs #notpresent — Know the Difference
Null vs Missing Key
# Key is MISSING entirely
* def foo = { }
* match foo == { a: '##null' }       # PASS
* match foo == { a: '#notpresent' }  # PASS

# Key EXISTS with null value
* def bar = { a: null }
* match bar == { a: '#null' }        # PASS
* match bar == { a: '#present' }     # PASS

# Key EXISTS with a value
* def baz = { a: 1 }
* match baz == { a: '#notnull' }     # PASS
* match baz == { a: '#present' }     # PASS
Regex Escaping
Use double backslashes in regex patterns: #regex a\\.dot matches the literal string a.dot. The first backslash escapes the second in Karate's string processing.
07

Schema Validation — Reusable Patterns

Define schemas once, reuse them everywhere. Simpler and more powerful than JSON Schema, with zero dependencies.

Reusable Schema · Define Once, Use Everywhere
Background:
  * def geoSchema =
    { lat: '#string', lng: '#string' }

  * def addressSchema =
    {
      street: '#string',
      suite: '#string',
      city: '#string',
      zipcode: '#regex \\d{5}-\\d{4}',
      geo: '#(geoSchema)'
    }

  * def companySchema =
    {
      name: '#string',
      catchPhrase: '#string',
      bs: '#string'
    }

  * def userSchema =
    """
    {
      id: '#number',
      name: '#string',
      username: '#string? _.length >= 3',
      email: '#regex .+@.+\\..+',
      address: '#(addressSchema)',
      phone: '#string',
      website: '#string',
      company: '#(companySchema)'
    }
    """

Scenario: Validate single user
  Given url 'https://jsonplaceholder.typicode.com'
  And path 'users', 1
  When method get
  Then status 200
  And match response == userSchema

Scenario: Validate all users
  Given url 'https://jsonplaceholder.typicode.com'
  And path 'users'
  When method get
  Then status 200
  And match each response == userSchema
Array Schema · #[] with Element Type
# Must be an array (any size)
* match response == '#[]'

# Exact array length
* match response == '#[10]'

# Array of strings
* match response == '#[] #string'

# Array of exactly 3 strings
* match items == '#[3] #string'

# Each element satisfies a predicate
* match items == '#[]? _.length == 1'

# Combine type + length + predicate
* match items == '#[] #string? _.length > 0'
External Schema Files
# Load schema from JSON file
* def userSchema = read('classpath:schemas/user.json')

* match response == userSchema

# Or from a .feature file
* def result = call read('classpath:common/schemas.feature')
* match response == result.userSchema
Pro Tip
Use #(schemaName) embedded expressions to nest schemas within schemas. This keeps your validation DRY and composable across your entire test suite.
08

Self-Validation #? & Custom Logic

#? · Custom Validation Expressions
# Range validation
* match product ==
  {
    price: '#number? _ > 0 && _ < 10000',
    discount: '#number? _ >= 0 && _ <= 100',
    quantity: '#number? _ > 0'
  }

# String length validation
* match user ==
  {
    username: '#string? _.length >= 3',
    bio: '##string? _.length <= 500'
  }

# External variable reference
* def min = 1
* def max = 100
* match value == { count: '#? _ >= min && _ <= max' }

# Enum validation
* def validStatuses = ['active', 'pending', 'disabled']
* match user ==
  { status: '#? validStatuses.includes(_)' }

# Date format check
* match resp ==
  { created: '#regex \\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}' }
Cross-Field Validation with $
# $ references JSON root
* def temperature =
  { celsius: 100, fahrenheit: 212 }

* match temperature ==
  {
    celsius: '#number',
    fahrenheit: '#? _ == $.celsius * 1.8 + 32'
  }

# Embedded expression (cleaner syntax)
* match temperature contains
  { fahrenheit: '#($.celsius * 1.8 + 32)' }

# Validation functions
* def isValidEmail =
  function(e) { return e.indexOf('@') > 0 }

* match user.email == '#? isValidEmail(_)'

# Complex function validation
* def isISO8601 = function(d) {
    var parsed = new Date(d);
    return !isNaN(parsed.getTime());
  }

* match resp.createdAt == '#? isISO8601(_)'
09

Response Variables & Status Assertions

Built-in Response Variables
VariableTypeDescription
responseJSON / XML / StringAuto-parsed response body
responseStatusintHTTP status code (200, 404, etc.)
responseTimelongResponse time in milliseconds
responseHeadersMap<List>All response headers
responseCookiesMapResponse cookies with metadata
responseBytesbyte[]Raw binary response body
responseTypeStringjson, xml, or string
requestTimeStamplongJava system time when request sent
Status & Response Assertions
# Simple status assertion
Then status 200

# Status with assert (for ranges)
* assert responseStatus >= 200 && responseStatus < 300

# Status from a list
* match [200, 201, 204] contains responseStatus

# Response type check
* match responseType == 'json'

# Response body as string
* match response == 'Health Check OK'
* match response contains 'OK'

# Response body size
* assert responseBytes.length > 0

# Configure expected status (for negative tests)
* configure responseStatusCheck = false
When method get
* match responseStatus == 404
* match response.error == 'Not Found'
10

Header, Cookie & Response Time Assertions

Header Assertions
# match header (case-insensitive shortcut)
* match header Content-Type contains 'application/json'
* match header Cache-Control == 'no-cache'
* match header X-Request-Id == '#notnull'

# Direct responseHeaders access
# (headers are Map of Lists, use [0])
* match responseHeaders['Content-Type'][0]
    contains 'json'

# Case-insensitive header lookup (recommended)
* def ct = karate.response.header('content-type')
* match ct contains 'json'

# Assert header exists
* match responseHeaders['X-Correlation-Id']
    == '#notnull'
Cookie & Response Time
# Cookie assertions
* match responseCookies.sessionId == '#notnull'
* match responseCookies.sessionId.value
    == '#string'
* match responseCookies.sessionId.path == '/'

# Response time (SLA compliance)
* assert responseTime < 2000

# Store time before next request resets it
* def firstCallTime = responseTime
When method get
* assert responseTime < firstCallTime * 2

# Performance budget
* match responseTime within
    { low: 0, high: 1500 }
Important
Response variables (response, responseTime, etc.) are reset after every HTTP call. Store values in def variables before making new requests.
11

XML Assertions

All fuzzy markers and match variants work with XML responses. Karate handles XPath natively.

XML · Fuzzy Matching
# XML with fuzzy markers
* def xml =
  """
  <root>
    <hello>world</hello>
    <id>123</id>
  </root>
  """

* match xml ==
  """
  <root>
    <hello>#string</hello>
    <id>#ignore</id>
  </root>
  """

# XML attribute matching
* def xml2 =
  <item id="123" status="active">content</item>

* match xml2 ==
  <item id="#string" status="#ignore">content</item>
XML · XPath & SOAP
# XPath extraction and match
* def name = karate.xmlPath(xml, '/root/hello')
* match name == 'world'

# SOAP response validation
* match response ==
  """
  <soap:Envelope xmlns:soap="...">
    <soap:Body>
      <GetUserResponse>
        <Name>#string</Name>
        <Email>#regex .+@.+</Email>
        <Id>#number</Id>
      </GetUserResponse>
    </soap:Body>
  </soap:Envelope>
  """

# Text matching (non-JSON, non-XML)
* match response == 'Health Check OK'
* match response contains 'OK'
* match response !contains 'Error'
12

JsonPath & Data Extraction

JsonPath · Wildcards & Filters
* def cat =
  """
  {
    "name": "Billie",
    "kittens": [
      { "id": 23, "name": "Bob" },
      { "id": 42, "name": "Wild" }
    ]
  }
  """

# Wildcard [*] returns arrays
* match cat.kittens[*].id == [23, 42]
* match cat.kittens[*].name == ['Bob', 'Wild']

# Deep scan .. (matches at any depth)
* match cat..name == ['Billie', 'Bob', 'Wild']

# JsonPath filter
* def bob = get[0] cat.kittens[?(@.id==23)]
* match bob.name == 'Bob'

# Array index
* match cat.kittens[0].id == 23
* match cat.kittens[1].name == 'Wild'
get Keyword & $ Shortcuts
# get keyword for extraction
* def names = get cat.kittens[*].name
* match names == ['Bob', 'Wild']

# get[0] for first match
* def first = get[0] cat.kittens
* match first.id == 23

# $ shortcut for response
* def ids = $response.users[*].id
* match ids == [1, 2, 3]

# $varName shortcut for other variables
* def data = { users: [{ n: 'A' }, { n: 'B' }] }
* def names = $data.users[*].n
* match names == ['A', 'B']

# karate.jsonPath for dynamic paths
* def path = '$.kittens[0].name'
* def val = karate.jsonPath(cat, path)
* match val == 'Bob'
13

JavaScript Function Assertions

Write complex assertion logic using inline JavaScript functions, karate object helpers, and functional transforms.

Custom JS Validator Functions
# Inline JS validation functions
* def isValidEmail = function(e) {
    return e.indexOf('@') > 0
        && e.indexOf('.') > e.indexOf('@')
  }

* def isValidUUID = function(s) {
    var regex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
    return regex.test(s)
  }

* def isISO8601 = function(d) {
    return !isNaN(new Date(d).getTime())
  }

# Use in #? expressions
* match user ==
  {
    id: '#? isValidUUID(_)',
    email: '#? isValidEmail(_)',
    created: '#? isISO8601(_)'
  }

# Multi-field validator
* def isPositiveInt = function(n) {
    return typeof n === 'number'
        && n > 0
        && n % 1 === 0
  }

* match each response ==
  { id: '#? isPositiveInt(_)' }
karate Object · Functional Helpers
# karate.filter — filter array then assert
* def admins = karate.filter(response,
    function(x){ return x.role == 'admin' })
* assert admins.length > 0

# karate.map — transform then assert
* def names = karate.map(response,
    function(x){ return x.name })
* match names contains 'John'

# karate.distinct — unique values
* def roles = karate.map(response,
    function(x){ return x.role })
* def unique = karate.distinct(roles)
* match unique == ['admin', 'user', 'guest']

# karate.sort then assert order
* def sorted = karate.sort(response,
    function(a, b){ return a.id - b.id })
* match sorted[0].id == 1

# karate.sizeOf
* assert karate.sizeOf(response) == 10

# karate.keysOf / karate.valuesOf
* def keys = karate.keysOf(response[0])
* match keys contains 'id'

# karate.lowerCase (case-insensitive compare)
* def lower = karate.lowerCase(response)
* match lower.name == 'john doe'

# karate.forEach with assertion side-effects
* karate.forEach(response, function(item, i){
    if (item.role == 'admin')
      karate.log('Admin:', item.name)
  })
14

Java Interop Assertions

Call Java classes directly from Karate for enterprise-grade custom validation, complex date parsing, crypto checks, and more.

Java.type() · Custom Validation Utils
# Call static Java methods
* def UUID = Java.type('java.util.UUID')
* def generated = UUID.randomUUID().toString()
* match generated == '#uuid'

# Timestamp validation with Java
* def System = Java.type('java.lang.System')
* def now = System.currentTimeMillis()
* assert response.timestamp <= now

# Date parsing and comparison
* def sdf = Java.type('java.text.SimpleDateFormat')
* def fmt = new sdf('yyyy-MM-dd')
* def parsed = fmt.parse(response.date)
* assert parsed != null

# Case-insensitive string comparison
* assert response.name.equalsIgnoreCase('JOHN DOE')

# Regex via Java Pattern
* def Pattern = Java.type('java.util.regex.Pattern')
* def p = Pattern.compile('^[A-Z]{2}\\d{4}$')
* assert p.matcher(response.code).matches()
Custom Java Utility Class
// src/test/java/utils/AssertionUtils.java
package utils;

import java.util.*;

public class AssertionUtils {

  public static boolean isValidPhone(String p) {
    return p.matches("^\\+?\\d{10,15}$");
  }

  public static boolean isSorted(List<Integer> list) {
    for (int i = 1; i < list.size(); i++) {
      if (list.get(i) < list.get(i-1)) return false;
    }
    return true;
  }

  public static boolean containsKey(
      Map<String,Object> map, String key) {
    return map.containsKey(key);
  }
}
Using Custom Java Utils in .feature
* def Utils = Java.type('utils.AssertionUtils')

* assert Utils.isValidPhone(response.phone)
* assert Utils.isSorted(response.ids)

# In #? expression
* match each response ==
  { phone: '#? Utils.isValidPhone(_)' }

# Match class (programmatic Java assertions)
// In Java code:
// Match.that(response).contains("{ id: 1 }");
// Match.that(city).isEqualTo("London");
15

karate.expect() — Chai-Style BDD Assertions

For teams migrating from Postman/Chai/Mocha, Karate v2 provides a familiar BDD assertion API. This is a convenience layer over match.

karate.expect() · Full API
# Equality
* karate.expect(response.name).to.equal('John')
* karate.expect(response.name).to.not.equal('Jane')

# Numeric comparisons
* karate.expect(response.age).to.be.above(18)
* karate.expect(response.age).to.be.below(100)
* karate.expect(response.score).to.be.at.least(1)
* karate.expect(response.score).to.be.at.most(100)

# Property checks
* karate.expect(response).to.have.property('email')
* karate.expect(response).to.not.have.property('password')

# Array/String includes
* karate.expect(response.tags).to.include('active')
* karate.expect(response.name).to.include('John')

# Type checks
* karate.expect(response.id).to.be.a('number')
* karate.expect(response.name).to.be.a('string')
* karate.expect(response.items).to.be.an('array')

# Length
* karate.expect(response.items).to.have.length(5)

# Truthiness
* karate.expect(response.active).to.be.ok
* karate.expect(response.deleted).to.not.be.ok
When to Use
Use karate.expect() when it improves readability for your team. For most Karate-native tests, match is more idiomatic and powerful.
match vs karate.expect() — Same Test
Side-by-Side Comparison
# match style (idiomatic Karate)
* match response.name == 'John'
* match response contains { email: '#string' }
* match response.tags contains 'active'
* assert response.age > 18

# karate.expect style (Chai-like)
* karate.expect(response.name).to.equal('John')
* karate.expect(response).to.have.property('email')
* karate.expect(response.tags).to.include('active')
* karate.expect(response.age).to.be.above(18)
16

Soft Assertions & Assertion Labels

Soft Assertions (v2)

By default, a failed match stops the scenario immediately. Soft assertions collect all failures before stopping.

continueOnStepFailure
Scenario: Validate all fields at once
  # Enable soft assertions
  * configure continueOnStepFailure = true

  * match response.name == 'Alice'
  * match response.age == 30
  * match response.role == 'admin'
  * match response.email contains '@'
  * match response.active == true

  # Disable — all 5 assertions checked,
  # failures collected, scenario fails at end
  * configure continueOnStepFailure = false
Assertion Labels (v2)

A comment (#) immediately before a match becomes the failure message label — making test reports human-readable.

Assertion Labels · Readable Failures
Scenario: Validate user response
  * def user = { name: 'bar', status: 'pending' }

  # user name should be baz
  * match user.name == 'baz'

  # Error message includes:
  # "user name should be baz"
  # "match failed: EQUALS - not equal"

  # this comment is ignored
  # status must be active
  * match user.status == 'active'
  # Only the LAST comment before match is used
Pro Tip
Combine soft assertions with assertion labels for maximum debugging efficiency. Each failure in your HTML report will show a human-readable description instead of cryptic match errors.
17

Retry Until & Polling Assertions

retry until · Polling Pattern
# Configure retry settings
* configure retry = { count: 5, interval: 2000 }

# Retry until status is 200
Given url 'https://api.example.com/job/123'
And retry until responseStatus == 200
When method get

# Retry until response field matches
Given url 'https://api.example.com/job/123'
And retry until response.status == 'complete'
When method get

# Complex JS expression
And retry until
  response.status == 'done' || response.status == 'failed'
When method get

# Default: 3 attempts, 3000ms interval
And retry until response.items.length > 0
When method get
Real-World Async Polling
Scenario: Wait for async job to complete
  # Start a background job
  Given url baseUrl + '/jobs'
  And request { type: 'export', format: 'csv' }
  When method post
  Then status 202
  * def jobId = response.jobId

  # Poll until complete (max 10 tries, 3s apart)
  * configure retry = { count: 10, interval: 3000 }
  Given url baseUrl + '/jobs/' + jobId
  And retry until
    response.status == 'complete' ||
    response.status == 'failed'
  When method get

  # Assert final state
  * match response.status == 'complete'
  * match response.downloadUrl == '#notnull'
Note
retry until must appear BEFORE the method step. The condition is a JavaScript expression that can reference response, responseStatus, and responseHeaders.
18

GraphQL, gRPC & WebSocket Assertions

GraphQL · Query & Assert
Scenario: GraphQL assertion
  Given url 'https://api.example.com/graphql'

  # Use text keyword for GraphQL (not def)
  And text query =
    """
    {
      user(id: 1) {
        id
        name
        email
        posts { title }
      }
    }
    """

  And request { query: '#(query)' }
  When method post
  Then status 200

  # Assert GraphQL response structure
  * match response.data.user ==
    {
      id: '#notnull',
      name: '#string',
      email: '#regex .+@.+',
      posts: '#array'
    }

  # Assert no GraphQL errors
  * match response.errors == '#notpresent'

  # Assert nested array
  * match each response.data.user.posts ==
    { title: '#string' }
WebSocket & Async Assertions
Scenario: WebSocket message assertion
  # Connect to WebSocket
  * def handler = function(msg) {
      return msg.contains('COMPLETE')
    }

  * def ws = karate.webSocket(wsUrl, handler)

  # Send message
  * ws.send('{ "action": "subscribe" }')

  # Wait for matching message (timeout: 5s)
  * def result = ws.listen(5000)
  * match result contains 'COMPLETE'

Scenario: gRPC assertion
  # gRPC uses same match assertions
  # Protobuf auto-converts to JSON
  Given url 'grpc://localhost:50051'
  And path 'myservice.MyMethod'
  And request { name: 'test' }
  When method post
  * match response ==
    { message: '#string', code: '#number' }
19

Data Manipulation for Assertions

set & remove · Modify Before Assert
# set — add or modify properties
* def user = { id: 1, name: 'John' }
* set user.active = true
* set user.profile.email = 'john@test.com'
* match user.active == true
* match user.profile.email == 'john@test.com'

# set multiple with table syntax
* def user = {}
* set user
  | path          | value              |
  | name.first    | 'John'             |
  | name.last     | 'Doe'              |
  | age           | 30                 |
* match user.name.first == 'John'

# remove — strip fields before assertion
* def resp = response
* remove resp.password
* remove resp.internalId
* match resp == expected

# remove array element by index
* def items = [1, 2, 3]
* remove items[1]
* match items == [1, 3]

# Verify removal
* match user.password == '#notpresent'
delete & Transform Before Assert
# delete — dynamic key removal
* def key = 'tempData'
* def obj = { id: 1, tempData: 'remove' }
* delete obj[key]
* match obj == { id: 1 }

# karate.merge — combine then assert
* def base = { id: 1 }
* def extra = { name: 'John' }
* def merged = karate.merge(base, extra)
* match merged == { id: 1, name: 'John' }

# karate.append — build arrays
* def arr = karate.append([1], [2], [3])
* match arr == [1, 2, 3]

# Transform XML to JSON for easier asserts
* def json = karate.toJson(xmlResponse)
* match json.root.name == 'John'

# Remove XML element
* def xml = <r><a>1</a><b>2</b></r>
* remove xml /r/b
* match xml == <r><a>1</a></r>
20

Contains Shortcuts & Embedded Symbols

Shortcut Symbols for Embedded Expressions
SymbolEquivalentUsage in #()
^contains#(^expected)
^^contains only#(^^expected)
^*contains any#(^*expected)
^+contains deep#(^+expected)
!^not contains#(!^expected)
Contains Shortcuts in Action
# Define expected schema
* def required = { id: '#number', title: '#string' }

# Response contains at minimum these fields
* match response == '#(^required)'

# Array contains all expected items (any order)
* def expected = [{ id: 42 }, { id: 23 }]
* match cat ==
  { name: 'Billie', kittens: '#(^^expected)' }

# Contains any of these
* def data = { tags: ['api', 'test'] }
* match data.tags == '#(^*["api", "other"])'

# Not contains
* match data.tags == '#(!^["deleted"])'
21

Real-World Assertion Recipes

Copy-paste patterns for the most common API assertion scenarios.

CRUD Validation
Create, Read, Update, Delete
# POST — Create
* match responseStatus == 201
* match response.id == '#notnull'
* match response contains request

# GET — Read
* match responseStatus == 200
* match response == schema

# PUT — Update
* match responseStatus == 200
* match response.updatedAt == '#notnull'

# DELETE — 204 No Content
* match responseStatus == 204
🔒
Error Response Validation
4xx / 5xx handling
# Disable auto status check for error tests
* configure responseStatusCheck = false

# 400 Bad Request
* match responseStatus == 400
* match response ==
  { error: '#string', message: '#string' }

# 401 Unauthorized
* match responseStatus == 401

# 404 Not Found
* match responseStatus == 404
* match response.error contains 'not found'
📄
Pagination Assertion
List endpoints with paging
* match response ==
  {
    data: '#[] #object',
    page: '#number',
    perPage: '#number',
    total: '#number',
    totalPages: '#number'
  }
* assert response.data.length <= response.perPage
* match each response.data contains
  { id: '#number' }
🔐
Auth Token Validation
JWT / OAuth patterns
# Login response
* match response ==
  {
    access_token: '#string',
    refresh_token: '#string',
    token_type: 'Bearer',
    expires_in: '#number? _ > 0'
  }

# JWT structure (3 dot-separated parts)
* match response.access_token ==
  '#regex [A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+'
📈
Sorting & Ordering
Verify sort order in responses
# Verify ascending sort
* def ids = $response[*].id
* def sorted = karate.sort(ids,
    function(a,b){ return a - b })
* match ids == sorted

# Or use Java interop
* def Utils = Java.type('utils.AssertionUtils')
* assert Utils.isSorted(ids)
🚀
Performance SLA
Response time budgets
# Hard SLA
* assert responseTime < 2000

# Range SLA with match within (v2)
* match responseTime within
  { low: 0, high: 1500 }

# Conditional SLA by endpoint
* def sla = (path == '/health') ? 500 : 3000
* assert responseTime < sla
📋
Data-Driven Assertions
Scenario Outline + Examples
Scenario Outline: Validate user <id>
  Given url baseUrl + '/users/' + <id>
  When method get
  Then status 200
  * match response.name == <name>
  * match response.role == <role>

  Examples:
    | id | name    | role    |
    | 1  | 'Alice' | 'admin' |
    | 2  | 'Bob'   | 'user'  |
    | 3  | 'Carol' | 'guest' |
🛠
Reusable Feature Call
Common assertion in shared file
# common/validate-user.feature (@ignore)
Scenario:
  * match user == userSchema
  * match user.email == '#regex .+@.+'

# In your test:
* def result = call read('common/validate-user.feature')
  { user: response }
😎
Negative Test Patterns
Validate what should NOT exist
# Field must NOT be in response
* match response.password == '#notpresent'
* match response.ssn == '#notpresent'

# Array must NOT contain value
* match response.roles !contains 'superadmin'

# Object must NOT have field
* match response !contains { debug: '#present' }

# Empty array check
* match response.errors == '#[0]'