Skip to main content

ASSERTIONS

Schema Validation

Validate complex API payloads using Karate's schema validation - simpler and more powerful than JSON-schema, with zero dependencies and inline assertions in a single step.

On this page:

Validation Markers

Karate provides built-in markers for type and format validation:

MarkerDescription
#ignoreSkip comparison for this field
#nullValue must be null (key must be present)
#notnullValue must not be null
#presentKey must be present (value can be anything, even null)
#notpresentKey must not be present at all
#arrayValue must be a JSON array
#objectValue must be a JSON object
#booleanValue must be true or false
#numberValue must be a number
#stringValue must be a string
#uuidValue must match UUID format
#regex STRValue must match the regular expression
#? EXPRJavaScript expression must evaluate to true
Gherkin
Feature: Validation markers

Scenario: Basic type validation
Given url 'https://jsonplaceholder.typicode.com'
And path 'users', 1
When method get
Then status 200
And match response ==
"""
{
id: '#number',
name: '#string',
username: '#string',
email: '#regex .+@.+',
address: '#object',
phone: '#string',
website: '#string',
company: '#object'
}
"""
Regex Escaping

Use double backslash for regex escaping: '#regex a\\.dot' matches 'a.dot'

Optional Fields

Prefix any marker with ## to make a field optional (key can be missing or value can be null):

Gherkin
Feature: Optional field validation

Scenario: Schema with optional fields
* def user = { id: 1, name: 'John', email: 'john@test.com' }

# middleName is optional - can be missing or null
* match user == { id: '#number', name: '#string', email: '#string', middleName: '##string' }

Null vs Not Present

Karate distinguishes between a key with null value and a missing key:

Gherkin
Feature: Null handling

Scenario: Understand null vs notpresent
* def withNull = { a: null }
* def empty = { }

# Key exists with null value
* match withNull == { a: '#null' }
* match withNull == { a: '#present' }

# Key does not exist
* match empty == { a: '#notpresent' }
* match empty == { a: '##null' }

# ##null matches both cases
* match withNull == { a: '##null' }
* match empty == { a: '##null' }

Array Validation

Use #[] for array validation with optional size and element type:

Gherkin
Feature: Array validation

Scenario: Array size and type patterns
Given url 'https://jsonplaceholder.typicode.com'
And path 'users'
When method get
Then status 200
# Must be an array (any size)
* match response == '#[]'
# Must be an array of exactly 10 items
* match response == '#[10]'
# Must be an array of objects
* match response == '#[] #object'

Array Element Validation

Combine array markers with element type validation:

Gherkin
Feature: Array element types

Scenario: Validate array element types
* def tags = ['api', 'test', 'karate']

# Array of strings
* match tags == '#[] #string'
# Array of exactly 3 strings
* match tags == '#[3] #string'
# Each string has length > 0
* match tags == '#[] #string? _.length > 0'

Match Each

Use match each to validate every element in an array against a schema:

Gherkin
Feature: Match each validation

Scenario: Validate each array element
Given url 'https://jsonplaceholder.typicode.com'
And path 'users'
When method get
Then status 200
And match each response ==
"""
{
id: '#number',
name: '#string',
username: '#string',
email: '#regex .+@.+',
address: '#object',
phone: '#string',
website: '#string',
company: '#object'
}
"""

Self-Validation Expressions

Use #? EXPR for custom validation logic. The underscore _ represents the current value:

Gherkin
Feature: Self-validation

Scenario: Custom validation with underscore
* def user = { name: 'John', age: 25 }

# Validate age is between 18 and 100
* match user == { name: '#string', age: '#? _ >= 18 && _ <= 100' }

# Combine type check with validation
* match user == { name: '#string? _.length > 0', age: '#number? _ > 0' }

Using Functions

Create reusable validation functions:

Gherkin
Feature: Function validation

Scenario: Validate with functions
* def isValidEmail = function(x) { return x.indexOf('@') > 0 }
* def user = { email: 'test@example.com' }

* match user == { email: '#? isValidEmail(_)' }

Using Variables in Expressions

Reference other variables in validation expressions:

Gherkin
Feature: Variable references

Scenario: Use variables in validation
* def minAge = 18
* def maxAge = 100
* def user = { age: 25 }

* match user == { age: '#? _ >= minAge && _ <= maxAge' }

Cross-Field Validation

Use $ to reference the JSON root for cross-field validation:

Gherkin
Feature: Cross-field validation

Scenario: Validate related fields
* def temperature = { celsius: 100, fahrenheit: 212 }

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

Match Each with Parent Reference

In match each, use _$ to reference the current array element:

Gherkin
Feature: Parent reference in match each

Scenario: Cross-field validation in arrays
* def orders =
"""
[
{ items: 3, total: 30, pricePerItem: 10 },
{ items: 5, total: 25, pricePerItem: 5 }
]
"""

# Validate total equals items * pricePerItem
* match each orders contains { total: '#? _ == _$.items * _$.pricePerItem' }
Symbol Reference
SymbolMeaning
$The JSON root document
_The current value being validated ("self")
_$The parent object in match each iterations

Reusable Schemas

Define schemas as variables and reuse them across scenarios:

Gherkin
Feature: Reusable schemas

Background:
* def geoSchema = { lat: '#string', lng: '#string' }
* def addressSchema = { street: '#string', suite: '#string', city: '#string', zipcode: '#string', geo: '#(geoSchema)' }
* def companySchema = { name: '#string', catchPhrase: '#string', bs: '#string' }
* def userSchema =
"""
{
id: '#number',
name: '#string',
username: '#string',
email: '#regex .+@.+',
address: '#(addressSchema)',
phone: '#string',
website: '#string',
company: '#(companySchema)'
}
"""

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

Embedded Schema References

Compose larger schemas out of smaller ones. There are two distinct forms — pick based on whether the field holds a single value or an array:

  • Single value — embed a schema with #(schemaName).
  • Array of values — validate every element with the #[] schemaName shortcut. The schema name is written bare after #[], not wrapped in #(...).
Gherkin
Feature: Embedded schemas

Scenario: Nested schema references
* def itemSchema = { id: '#number', name: '#string' }
* def addressSchema = { city: '#string', zip: '#string' }

* def orderSchema =
"""
{
orderId: '#number',
shipTo: '#(addressSchema)',
items: '#[] itemSchema'
}
"""

* def order = { orderId: 1, shipTo: { city: 'NYC', zip: '10001' }, items: [{ id: 1, name: 'Widget' }] }
* match order == orderSchema
#[] takes a bare schema name

Write items: '#[] itemSchema'not items: '#[] #(itemSchema)'. After the #[] array marker the remainder is already evaluated as the per-element schema, so wrapping it in #(...) is unnecessary and can misfire. Reserve #(...) for embedding a schema as a single value, like shipTo: '#(addressSchema)'.

Optional Nested Schemas

A nested object that may legitimately be null or absent — common for fields gated on a user's role or account type — takes the ## prefix, just like ##string does for a primitive. Write it inline on the right-hand side of the match:

Gherkin
Feature: Optional nested schema

Scenario: A nullable nested object
* def detailsSchema = { name: '#string', id: '#number' }

# all three pass — null, absent, or a value that matches
* match { status: 'active', details: null } == { status: '#string', details: '##(detailsSchema)' }
* match { status: 'active' } == { status: '#string', details: '##(detailsSchema)' }
* match { status: 'active', details: { name: 'x', id: 1 } } == { status: '#string', details: '##(detailsSchema)' }

## only excuses null and a missing key. A details that is present still has to match detailsSchema.

Wrap the schema in ( ) to re-use it

##(...) does double duty. When you are building data, it means "substitute this value, and drop the key if it is null" — so a plain def resolves the reference and inlines the schema, which quietly turns the field back into a required one:

Gherkin
# WRONG - '##(detailsSchema)' is inlined at def time, the '##' is lost
* def mainSchema = { status: '#string', details: '##(detailsSchema)' }
* match { status: 'active', details: null } == mainSchema # fails

Wrap the JSON in round brackets to keep the placeholder intact until the match:

Gherkin
# RIGHT - the ( ) preserve '##(detailsSchema)' for the match engine
* def mainSchema = ({ status: '#string', details: '##(detailsSchema)' })
* match { status: 'active', details: null } == mainSchema # passes

This applies only to ##(schemaName). Required references such as '#(addressSchema)' and array shortcuts such as '#[] itemSchema' need no brackets — see Schema as a Reusable Template below.

For XML, the equivalent is to load the schema as text first and convert it with xml — which likewise skips the substitution that a def would do:

Gherkin
* def subSchema = <c>#string</c>
* text schema =
"""
<root>
<a>#string</a>
<b>##(subSchema)</b>
</root>
"""
* xml schema = schema
* match <root><a>x</a></root> == schema

If the schema lives in its own file, see Cross-Referenced Schema Filesread() is a data-building path, so it inlines ##(...) just like a plain def does.

Schema as a Reusable Template

A #(...) placeholder inside a schema doesn't have to be resolvable when the schema is defined. If the referenced variable isn't in scope yet at def time, Karate preserves the placeholder and resolves it later — when the schema is actually used in a match. This lets a shared schema (in a file, or a Background) be loaded before the caller supplies the values it depends on:

Gherkin
Feature: Schema as a template

Scenario: Placeholder resolved at match time
# idCheck isn't defined yet — the #(idCheck) placeholder is preserved, not an error
* def userSchema =
"""
{
id: '#(idCheck)',
name: '#string'
}
"""

# the caller decides what 'id' must look like, later
* def idCheck = '#number'

* def user = { id: 42, name: 'Alice' }
* match user == userSchema

If the placeholder is still unresolved when the match runs, the error surfaces there — at match time — rather than being silently swallowed.

External Schema Files

Load schemas from JSON files for larger projects:

Gherkin
Feature: External schemas

Scenario: Load schema from file
* def userSchema = read('classpath:schemas/user-schema.json')

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

Cross-Referenced Schema Files

Once schemas live in files, one of them usually references another — an entity schema whose address is described by an address schema. A required reference works with a plain read(), because inlining the nested schema is exactly what you want:

matchers/entity.json
{
"id": "#string",
"address": "#(addressSchema)"
}
Gherkin
* def addressSchema = read('matchers/address.json')
* def entitySchema = read('matchers/entity.json')

An optional reference — ##(addressSchema), for an address that may be null or absent — is different. read() is a data-building path, so it resolves the reference and inlines the nested schema, and the ## is consumed along the way; address ends up plain required. It is the same trap as a bracket-less def, described under Optional Nested Schemas above.

To keep the marker intact, read the file as raw text with karate.readAsString() and convert it with the json keyword. json parses the text exactly as authored — it does not resolve embedded expressions — so ##(addressSchema) survives to the match engine:

matchers/address.json
{
"address1": "#string",
"address2": "##string",
"city": "#string",
"postalCode": "#string"
}
matchers/entity.json
{
"id": "#string",
"address": "##(addressSchema)"
}
Gherkin
Feature: Cross-referenced schema files

Background:
* def addressSchema = read('matchers/address.json')
* json entitySchema = karate.readAsString('matchers/entity.json')

Scenario: address may be null, absent, or a full object
* match { id: 'x', address: null } == entitySchema
* match { id: 'x' } == entitySchema
* match { id: 'x', address: { address1: 'a', city: 'c', postalCode: 'p' } } == entitySchema

This is the JSON twin of the text then xml idiom shown for XML schemas above.

Order doesn't matter

Because the reference is resolved at match time, addressSchema only has to be in scope when the match runs. The two Background lines can be in either order, and a schema can even be loaded before the feature that supplies its dependencies. If the nested schema was never defined at all, the failure surfaces at the match and names the variable.

Mixing the two is fine, and often what you want: use read() for schema files that only contain required references, and the readAsString() + json pair for the ones carrying an optional ##(...) reference.

Contains Short-Cuts

Use these symbols in embedded expressions for contains-style matching:

SymbolEquivalent
^contains
^^contains only
^*contains any
^+contains deep
!^not contains
Gherkin
Feature: Contains short-cuts

Scenario: Use contains in schema validation
* def cat =
"""
{
name: 'Billie',
kittens: [
{ id: 23, name: 'Bob' },
{ id: 42, name: 'Wild' }
]
}
"""
* def expected = [{ id: 42, name: 'Wild' }, { id: 23, name: 'Bob' }]

# Validate kittens contains all expected items in any order
* match cat == { name: 'Billie', kittens: '#(^^expected)' }

Complete Schema Example

Combine all techniques for comprehensive validation:

Gherkin
Feature: Complete schema validation

Scenario: Validate complex API response
Given url 'https://jsonplaceholder.typicode.com'
And path 'users', 1
When method get
Then status 200
And match response ==
"""
{
id: '#number',
name: '#string',
username: '#string? _.length >= 3',
email: '#regex .+@.+\\..+',
address: {
street: '#string',
suite: '#string',
city: '#string',
zipcode: '#regex \\d{5}-\\d{4}',
geo: {
lat: '#string',
lng: '#string'
}
},
phone: '#string',
website: '#string',
company: {
name: '#string',
catchPhrase: '#string',
bs: '#string'
}
}
"""

Next Steps