Advanced Karate Schema Matching Guide
Use advanced Karate schema matching to validate nested JSON, fuzzy fields, reusable schemas, and API response contracts without brittle assertions.
Advanced Karate Schema Matching Guide
The response body is valid JSON, the status is 200, and the test still tells you almost nothing. In Karate, the difference between a shallow API smoke check and a useful contract-style assertion is usually the match expression: what shape you validate, how much dynamic data you tolerate, and whether nested arrays are checked as real collections instead of ignored blobs.
Karate's schema matching is not a separate plugin. It is built into the DSL through fuzzy markers such as #string, #number, #boolean, #uuid, predicates like #? _ > 0, optional markers, and structural match operators such as contains, contains deep, and match each. The hard part is not memorizing the markers. The hard part is choosing an assertion shape that catches broken payloads without turning every timestamp, generated id, and optional field into a brittle exact match.
This guide assumes you can already send requests with Karate. For a broader framework walkthrough, read Karate DSL API testing guide 2026. For positioning Karate against other API tools, use Karate API testing framework guide.
Make the Contract Visible in the Feature File
Karate lets you define JSON fragments directly in a feature file. That is useful because reviewers can see the response contract beside the request that exercises it. The following example validates an order summary where ids and dates are dynamic, line items are nested, and totals must satisfy business constraints.
Feature: order summary schema
Background:
* url baseUrl
* def money = { amount: '#number', currency: '#regex [A-Z]{3}' }
* def lineItem =
"""
{
sku: '#string',
name: '#string',
quantity: '#? _ > 0',
unitPrice: '#(money)',
discounts: '#[]'
}
"""
* def orderSummary =
"""
{
id: '#uuid',
status: '#regex CREATED|PAID|CANCELLED',
createdAt: '#? new Date(_).toString() != "Invalid Date"',
customer: {
id: '#string',
email: '#regex .+@.+\\..+'
},
items: '#[] lineItem',
total: '#(money)'
}
"""
Scenario: created order returns a stable nested schema
Given path 'orders'
And request { sku: 'QA-BOOK', quantity: 2 }
When method post
Then status 201
And match response == orderSummary
And match response.items[0] contains { sku: 'QA-BOOK', quantity: 2 }
And match response.total.amount == '#? _ > 0'
The schema is strict where strictness matters. The response must have an id shaped like a UUID, a recognized status, parseable creation time, customer fields, an array of line items, and a positive total. The test does not care about the exact id or timestamp because those are generated by the system.
Fuzzy Markers That Carry Most API Suites
Karate's markers are compact, which is both a strength and a risk. A response schema with too many #ignore markers becomes documentation that the team has stopped caring. Prefer the most specific marker you can defend.
| Marker or pattern | Validates | Good use | Misuse to avoid |
|---|---|---|---|
#string | Value is a string | Names, ids that are not UUIDs, enum-like labels | Dates that need parseability |
#number | Value is numeric | Amounts, counts, coordinates | Numeric strings from legacy APIs |
#boolean | Value is true or false | Feature flags, consent values | Tri-state fields that also allow null |
#uuid | UUID-shaped string | Resource identifiers | Custom ids with prefixes |
#regex ... | String matches a regular expression | Currency codes, slugs, formatted references | Complex parsing that belongs in a helper |
#? expression | JavaScript predicate returns true | Ranges, date parse checks, cross-field checks with care | Long business rules that hide readability |
##string | Optional string | Fields present only for some roles | Required fields that are inconvenient in fixtures |
#[] schema | Array of values matching schema | Homogeneous lists | Arrays where each item has different shape |
#ignore | Accept any value | Server-generated opaque token | Large sections nobody wants to validate |
Optional markers deserve discipline. If a field is optional because the API contract says so, use an optional marker. If it is optional because fixtures are inconsistent, fix the fixture or split the scenario.
Matching Nested Arrays Without Flattening the API
Many API bugs hide inside arrays. A shipment can contain the right top-level order id and still attach the wrong item quantity. Karate's match each and contains deep let you validate nested arrays without writing loops.
Feature: shipment allocation shape
Background:
* url baseUrl
* def allocationSchema =
"""
{
warehouseId: '#string',
carrier: '#regex DHL|UPS|FEDEX|LOCAL',
packages: '#[]',
items: '#[]'
}
"""
Scenario: each shipment allocation includes package tracking and item quantity
Given path 'orders', orderId, 'shipments'
When method get
Then status 200
And match response.allocations == '#[] allocationSchema'
And match each response.allocations contains deep
"""
{
packages: '#[]',
items: '#[]'
}
"""
And match each response.allocations[*].packages[*] contains
"""
{
trackingNumber: '#string',
weightGrams: '#? _ > 0'
}
"""
And match response.allocations[*].items[*].quantity contains only '#? _ > 0'
This example is intentionally nested because real response contracts are nested. A weak version would only assert that allocations is an array. A brittle version would compare the entire response to a fixture with exact tracking numbers. The useful version validates structure, allowed carrier values, package tracking fields, and positive quantities.
Exact, Contains, Deep Contains, and Each
Karate gives you several match operators. Treat them as different assertion strengths, not interchangeable syntax.
| Operator | What it checks | Useful when | Example intent |
|---|---|---|---|
match actual == expected | Complete equality with fuzzy marker support | The response shape is the contract | Order summary must contain no surprise top-level fields |
match actual contains expected | Actual includes the expected fragment at that level | Response has extra fields you do not care about | User profile contains the edited display name |
match actual contains deep expected | Nested structures include the expected fragment | You need to find a nested object without indexing through every parent | Shipment allocation has a package with tracking metadata |
match each array == schema | Every element matches the schema exactly | Homogeneous arrays | Every review has rating, author, and text |
match each array contains fragment | Every element includes the fragment | Items may have extra fields | Every product includes an inventory object |
match array contains only values | Array has only the listed values, order independent | Enum lists or role sets | User roles are exactly admin and auditor |
match array contains any values | Array has at least one of the listed values | Capability negotiation | Server offers at least one supported auth method |
The phrase "at that level" matters. contains is not the same as contains deep. When a test unexpectedly fails, first check whether the fragment is nested deeper than the operator searches.
Reusable Schemas Without Creating a New Language
Karate supports shared feature files and JavaScript helpers, but schema reuse can go too far. If a reviewer has to jump across five files to understand one response assertion, the test has lost one of Karate's advantages.
Use local schemas when the contract is specific to one endpoint. Promote a schema to a shared file when multiple endpoints truly return the same shape. Keep names concrete: money, address, pagination, problemDetails. Avoid names like commonResponse that become junk drawers.
Feature: shared schema fragments
Background:
* def schemas = call read('classpath:schemas/common.feature')
* def address = schemas.address
* def problem = schemas.problemDetails
Scenario: account endpoint returns billing address
Given url baseUrl
And path 'accounts', accountId
When method get
Then status 200
And match response.billingAddress == address
Scenario: missing account uses problem details
Given url baseUrl
And path 'accounts', 'missing-account'
When method get
Then status 404
And match response == problem
And match response.code == 'ACCOUNT_NOT_FOUND'
A shared schema should be boring. If it includes endpoint-specific branches, it is probably hiding too much variation. Split it or keep the assertion local.
Predicates for Business Boundaries
#? predicates are powerful because they can express range checks and parse checks inline. They are also a readability trap. A predicate like #? _ >= 0 && _ <= 100 is clear. A predicate with date math, multiple field references, and conditional branching is a maintenance risk.
Use predicates for local constraints:
| Constraint | Predicate shape | Why it belongs in schema |
|---|---|---|
| Positive count | #? _ > 0 | Validates numeric boundary at the field |
| Percentage range | #? _ >= 0 && _ <= 100 | Keeps range beside field definition |
| ISO-like date parse | #? new Date(_).toString() != "Invalid Date" | Avoids exact timestamp |
| Non-empty array | #? _.length > 0 | Captures collection expectation |
| String prefix | #? _.startsWith("ord_") | Documents custom id convention |
Move predicates into named helpers when the expression has business language. For example, tax jurisdiction eligibility should not live as a dense JavaScript expression inside a schema. Name the helper and test it separately if it is complex.
Validating Error Payloads With the Same Care
Teams often validate success schemas carefully and treat error responses as status-only checks. That misses contract drift where clients break because an error code disappears or the field path changes. Karate makes error schema validation cheap.
Feature: validation error schema
Background:
* url baseUrl
* def fieldError =
"""
{
field: '#string',
message: '#string',
code: '#regex REQUIRED|INVALID|TOO_SMALL|TOO_LARGE'
}
"""
* def validationProblem =
"""
{
type: '#string',
title: 'Validation failed',
status: 400,
traceId: '#string',
errors: '#[] fieldError'
}
"""
Scenario: invalid checkout request reports field-level errors
Given path 'checkout'
And request { email: 'not-an-email', quantity: 0 }
When method post
Then status 400
And match response == validationProblem
And match response.errors contains { field: 'email', code: 'INVALID', message: '#string' }
And match response.errors contains { field: 'quantity', code: 'TOO_SMALL', message: '#string' }
This is not just defensive testing. Error payloads are part of the API. Frontend clients, mobile apps, and partner integrations often depend on stable error codes more than they depend on exact success response ordering.
Debugging Schema Failures
Karate's mismatch output is usually direct, but nested arrays can still produce noisy failures. Reduce the assertion to the smallest failing path. If response fails against a full schema, try response.items[0] against the item schema. If contains deep fails, assert the intermediate array path exists before matching the fragment.
Three habits make debugging faster. First, name schemas so failure messages have meaningful context in the feature file. Second, assert important collection sizes when the count is part of the behavior. Third, avoid giant golden response files unless the endpoint is truly static. Golden files tend to create massive diffs where the actual contract failure is one field.
Schema Matching for Pagination and Partial Updates
Pagination responses deserve their own schema because they combine collection shape with navigation metadata. A common mistake is to validate only response.data and ignore pageInfo, which lets clients break when nextCursor, hasMore, or count fields drift.
Feature: paginated customer search schema
Background:
* url baseUrl
* def customer =
"""
{
id: '#string',
displayName: '#string',
status: '#regex ACTIVE|SUSPENDED|INVITED'
}
"""
* def pageInfo =
"""
{
limit: '#? _ >= 1 && _ <= 100',
hasMore: '#boolean',
nextCursor: '##string'
}
"""
Scenario: customer search returns items and cursor metadata
Given path 'customers'
And param limit = 25
When method get
Then status 200
And match response ==
"""
{
data: '#[] customer',
pageInfo: '#(pageInfo)'
}
"""
And match response.pageInfo.limit == 25
Partial update endpoints need a different style. The request may include one field, but the response often returns a full resource. Use contains for the changed fields and a full schema for the resource if the endpoint promises a complete representation. If the endpoint returns only a patch result, assert that narrow result instead of forcing a full-resource schema where it does not belong.
| Endpoint behavior | Better Karate assertion | Reason |
|---|---|---|
| PATCH returns full resource | match response == resourceSchema and contains changed fields | Protects full response contract |
| PATCH returns changed fields | match response == patchResultSchema | Avoids inventing fields |
| PUT replaces resource | Exact schema plus important field equality | Replacement should be complete |
| Bulk update returns per-row outcomes | match each response.results == rowOutcome | Catches partial failures |
| Search returns paginated data | Validate item schema and metadata schema | Clients depend on both |
Advanced Karate suites become easier to review when each assertion matches the endpoint's response promise. Do not use the same full-resource schema everywhere just because it exists.
Keeping Schema Assertions Readable in Large Features
Karate makes it easy to place schemas inline, but very large schemas can bury the scenario. A practical pattern is to keep tiny fragments in the Background, keep endpoint-specific response schemas near the scenario, and move only genuinely shared fragments to a classpath feature. The reader should be able to answer two questions without hunting: what request did we send, and what response contract did we verify?
When a schema grows past the visible page, split by domain concept rather than by JSON depth. For an invoice response, fragments such as invoiceHeader, invoiceLine, taxBreakdown, and paymentState are easier to review than part1, part2, and nestedFields. Names should match product language.
| Schema organization | Review outcome |
|---|---|
| Product-named fragments | Reviewer can map assertions to API documentation |
| Depth-based fragments | Reviewer must reconstruct meaning mentally |
| One shared mega-schema | Endpoint differences get hidden |
| Local scenario schema | Best for one endpoint or one role-specific response |
| Shared common fragment | Best for repeated stable concepts |
Also watch for schemas that become too permissive over time. A sequence of small fixes can turn exact fields into optional fields, optional fields into ignored fields, and arrays into untyped #[]. Schedule periodic review of high-value API schemas and ask whether each fuzzy marker still represents a real contract decision.
Cross-Field Checks Without Turning Karate Into Application Code
Some API rules involve more than one field. A refunded order should have status: REFUNDED and a non-empty refund object. A paid invoice should have a paid timestamp. Karate can express these checks, but keep them short and visible.
Scenario: paid invoice includes payment timestamp
Given path 'invoices', invoiceId
When method get
Then status 200
And match response.status == 'PAID'
And match response.paidAt == '#? new Date(_).toString() != "Invalid Date"'
And match response.balance.amount == 0
If the cross-field rule needs branching, calculations, or external lookup, move it to a named helper or a lower-level unit test. Karate should verify the API contract the client sees, not become a duplicate implementation of server policy.
One final review habit helps: read the assertion aloud as a client expectation. If it sounds like "the invoice API returns a paid timestamp when status is paid," it belongs in Karate. If it sounds like "the accounting engine computes revenue recognition," push it lower in the test pyramid. That boundary keeps API tests sharp.
Frequently Asked Questions
Is Karate schema matching the same as JSON Schema validation?
No. Karate schema matching is an assertion DSL inside Karate, using fuzzy markers and match operators. JSON Schema is a separate standard with its own vocabulary. Karate is often faster to read in tests, while JSON Schema can be useful when the schema is shared outside the test suite.
When should I use contains deep instead of contains?
Use contains deep when the fragment you care about can appear inside nested objects or arrays. Use contains when the expected fragment is at the current level. Choosing the narrower operator makes failures easier to understand.
Are optional markers safe for backward-compatible fields?
They are safe when optionality is part of the API contract. They are not safe as a workaround for unstable fixtures. If a field should always be present for a scenario, assert it as required even if other scenarios omit it.
How do I keep Karate schemas reusable without hiding too much?
Share small fragments that represent real repeated structures, such as money, address, pagination, or problem details. Keep endpoint-specific response schemas near the scenario unless several endpoints genuinely return the same contract.
Should error responses get schema checks too?
Yes. Error codes, field paths, trace ids, and problem detail shapes are client-facing contracts. A status-only test can pass while a mobile client loses the information it needs to display a useful validation message.