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
- Learn about Fuzzy Matching for flexible validation
- Explore Reusability for schema reuse
- Understand Advanced Features for complex scenarios