Skip to main content

ASSERTIONS

Schema Validation

Overview

Karate provides a simpler and more powerful alternative to JSON Schema for validating complex data structures. You can combine domain validation, conditional logic, and perform comprehensive assertions in a single step.

Array Validation Basics

Simple Array Validation

# Check if value is an array
* def foo = ['bar', 'baz']
* match foo == '#[]'
* match foo == '#[2]' # Array with exactly 2 elements
* match foo == '#[_ > 0]' # Non-empty array
* match foo == '#[_ >= 2 && _ <= 10]' # Array with 2-10 elements

Array Element Types

# Validate array element types
* def numbers = [1, 2, 3, 4, 5]
* match numbers == '#[] #number'
* match each numbers == '#number'

* def strings = ['apple', 'banana', 'orange']
* match strings == '#[] #string'
* match each strings == '#string'

* def mixed = [1, 'two', true, null]
* match mixed == '#[4]' # Just check length

Match Each

Uniform Array Validation

# Validate each element has same structure
* def users = [
{ id: 1, name: 'John', active: true },
{ id: 2, name: 'Jane', active: false },
{ id: 3, name: 'Bob', active: true }
]

* match each users == {
id: '#number',
name: '#string',
active: '#boolean'
}

Complex Element Validation

# Advanced validation for each element
* match each users == {
id: '#? _ > 0',
name: '#regex [A-Za-z]+',
active: '#boolean',
email: '##string' # Optional field
}

# Nested structure validation
* def orders = [
{
id: 1,
items: [
{ product: 'A', quantity: 2 },
{ product: 'B', quantity: 1 }
]
}
]

* match each orders == {
id: '#number',
items: '#[] #object'
}

* match each orders[*].items == {
product: '#string',
quantity: '#? _ > 0'
}

Complex Schema Patterns

Nested Schema Validation

# Define complex schema
* def orderSchema = {
id: '#uuid',
customer: {
name: '#string',
email: '#regex .+@.+',
address: {
street: '#string',
city: '#string',
zip: '#regex \\d{5}'
}
},
items: '#[] #object',
total: '#? _ > 0',
status: '#? ["pending", "processing", "completed", "cancelled"].contains(_)'
}

# Validate response
* match response == orderSchema

Conditional Schema

# Different validation based on type
* def productSchema = {
id: '#number',
type: '#string',
name: '#string',
# If type is 'book', validate additional fields
isbn: '#? $.type == "book" ? _ != null : true',
author: '#? $.type == "book" ? _ != null : true',
# If type is 'electronics', validate different fields
warranty: '#? $.type == "electronics" ? _ != null : true',
voltage: '#? $.type == "electronics" ? _ != null : true'
}

* match each products == productSchema

Recursive Schema

# Tree structure validation
* def treeNodeSchema = {
id: '#number',
name: '#string',
children: '##[] #object' # Optional array of objects
}

# Validate nested tree
* def tree = {
id: 1,
name: 'root',
children: [
{
id: 2,
name: 'child1',
children: []
},
{
id: 3,
name: 'child2',
children: [
{ id: 4, name: 'grandchild', children: [] }
]
}
]
}

* match tree == treeNodeSchema
* match each tree.children == treeNodeSchema

Array Size Validation

Fixed Size Arrays

# Exact size
* def triple = [1, 2, 3]
* match triple == '#[3]'
* match triple == '#[3] #number'

# Size with validation
* def codes = ['ABC', 'DEF', 'GHI']
* match codes == '#[3] #string'
* match codes == '#[3] #regex [A-Z]{3}'

Dynamic Size Validation

# Size based on another field
* def response = {
count: 5,
items: [1, 2, 3, 4, 5]
}

* match response == {
count: '#number',
items: '#[$.count]'
}

# Minimum/maximum size
* def items = [1, 2, 3, 4, 5]
* match items == '#[_ >= 3 && _ <= 10]'
* match items == '#[_ > 0]' # Non-empty

Schema with Functions

Custom Validation Functions

# Define validation functions
* def isValidEmail = function(email) {
return email && email.match(/.+@.+\..+/);
}

* def isValidDate = function(date) {
return !isNaN(Date.parse(date));
}

# Use in schema
* def userSchema = {
email: '#? isValidEmail(_)',
birthDate: '#? isValidDate(_)',
age: '#? _ >= 0 && _ <= 120'
}

* match response == userSchema

Reusable Validators

# Create validator library
* def validators =
"""
{
isUUID: function(v) {
return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(v);
},
isISO8601: function(v) {
return /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/.test(v);
},
isPositive: function(v) {
return typeof v === 'number' && v > 0;
}
}
"""

# Apply validators
* match response == {
id: '#? validators.isUUID(_)',
timestamp: '#? validators.isISO8601(_)',
amount: '#? validators.isPositive(_)'
}

Partial Schema Matching

Contains with Schema

# Partial schema matching
* def response = {
id: 1,
name: 'Product',
price: 99.99,
metadata: { created: '2024-01-01', updated: '2024-01-02' }
}

# Match contains with schema
* match response contains {
name: '#string',
price: '#? _ > 0'
}

# Deep contains with schema
* match response contains deep {
metadata: {
created: '#string'
}
}

Optional Fields

# Schema with optional fields
* def flexibleSchema = {
required1: '#string',
required2: '#number',
optional1: '##string', # Optional string
optional2: '##number', # Optional number
optional3: '##array' # Optional array
}

# Both match the schema
* match { required1: 'test', required2: 123 } == flexibleSchema
* match { required1: 'test', required2: 123, optional1: 'extra' } == flexibleSchema

Advanced Array Patterns

Array Transformation Validation

# Validate array after transformation
* def prices = [10, 20, 30, 40, 50]
* def total = prices.reduce((sum, p) => sum + p, 0)
* match total == 150

# Validate filtered array
* def users = [
{ name: 'Alice', age: 25, active: true },
{ name: 'Bob', age: 30, active: false },
{ name: 'Charlie', age: 35, active: true }
]

* def activeUsers = users.filter(u => u.active)
* match activeUsers == '#[2]'
* match each activeUsers == { name: '#string', age: '#number', active: true }

Grouped Validation

# Validate grouped data
* def transactions = [
{ type: 'credit', amount: 100 },
{ type: 'debit', amount: 50 },
{ type: 'credit', amount: 200 },
{ type: 'debit', amount: 75 }
]

# Group by type
* def grouped = {}
* eval transactions.forEach(t => {
if (!grouped[t.type]) grouped[t.type] = [];
grouped[t.type].push(t);
})

* match grouped == {
credit: '#[2] #object',
debit: '#[2] #object'
}

* match each grouped.credit == { type: 'credit', amount: '#? _ > 0' }
* match each grouped.debit == { type: 'debit', amount: '#? _ > 0' }

Schema Composition

Combining Schemas

# Base schemas
* def addressSchema = {
street: '#string',
city: '#string',
zip: '#regex \\d{5}',
country: '#string'
}

* def contactSchema = {
email: '#regex .+@.+',
phone: '#regex \\d{10}',
address: addressSchema
}

# Composed schema
* def personSchema = karate.merge(contactSchema, {
id: '#uuid',
name: '#string',
age: '#? _ >= 0 && _ <= 120'
})

* match response == personSchema

Schema Inheritance

# Base entity schema
* def baseEntitySchema = {
id: '#uuid',
created: '#string',
updated: '#string',
version: '#number'
}

# Extended schemas
* def userSchema = karate.merge(baseEntitySchema, {
username: '#string',
email: '#regex .+@.+',
roles: '#[] #string'
})

* def productSchema = karate.merge(baseEntitySchema, {
name: '#string',
price: '#? _ > 0',
stock: '#? _ >= 0'
})

Performance Validation

Response Time in Schema

# Include performance metrics in validation
* def performanceSchema = {
data: '#array',
meta: {
count: '#number',
responseTime: '#? _ < 1000', # Must be under 1 second
cached: '#boolean'
}
}

* match response == performanceSchema
* assert responseTime < 1000

Best Practices

Schema Organization

# ✅ Good: Modular schemas
Background:
* def schemas = read('classpath:schemas/common-schemas.json')
* def userSchema = schemas.user
* def orderSchema = schemas.order

# ✅ Good: Named schemas for clarity
* def validationSchema = {
user: userSchema,
orders: '#[] #object'
}

Schema Documentation

# ✅ Good: Document complex schemas
* def apiResponseSchema = {
# Required fields
status: '#? ["success", "error"].contains(_)',
code: '#number',

# Optional error details
error: '##object',

# Pagination meta
meta: {
page: '#? _ >= 1',
pageSize: '#? _ > 0 && _ <= 100',
total: '#? _ >= 0'
},

# Main data payload
data: '#array'
}

Error Handling

# ✅ Good: Validate error responses
* def errorSchema = {
error: '#string',
code: '#string',
details: '##array',
timestamp: '#string'
}

* if (responseStatus != 200) match response == errorSchema

Next Steps