Kotest JSON Matchers

Explains JSON matchers.

Overview

To use JSON matchers, add testImplementation("io.kotest.extensions:kotest-assertions-json:<version>") to your build.

Basic Matchers

Matcher Description Target
shouldBeValidJson Checks that the given string parses as valid JSON. Multiplatform
shouldBeJsonObject Checks that the string is a valid JSON object. Multiplatform
shouldBeJsonArray Checks that the string is a valid JSON array. Multiplatform

Content-Based Matching

See JSON content matching for more details.

Matcher Description Target
shouldEqualJson Checks that a string matches the specified JSON structure. Multiplatform
shouldEqualSpecifiedJson Checks that a string matches the specified JSON structure while allowing unspecified extra properties. Multiplatform
shouldContainJsonKey Checks that a string is JSON and contains the specified JSON path. JVM
shouldContainJsonKeyValue Checks that a string is JSON and contains the specified JSON path with the specified value. JVM
shouldMatchJsonResource Checks that a string matches the JSON content of the specified test resource. JVM

Schema Validation

Matcher Description Target
shouldMatchSchema Checks that a String or kotlinx.serialization.JsonElement matches a JsonSchema. See below for schema configuration. Multiplatform

JSON Content Matching

This module is available for JVM and JS targets.

shouldEqualJson

json.shouldEqualJson(other) checks that the JSON structure on the left is the same as the one on the right.

This matcher allows different formatting and different key order.

For example, the following two JSON strings are considered equal:

{
   "name": "sam",
   "location": "chicago",
   "age" : 41
}

and

{ "age" : 41, "name": "sam", "location": "chicago" }

The opposite of this matcher is shouldNotEqualJson, which fails if the two JSON strings are considered equal.

compareJsonOptions

shouldEqualJson supports an additional parameter of type CompareJsonOptions, which provides flags for toggling JSON comparison behavior.

Usage:
Options can be specified inline:

a.shouldEqualJson(b, compareJsonOptions { arrayOrder = ArrayOrder.Strict })

Another option is to define the comparison behavior as desired:

val myOptions = compareJsonOptions {
   typeCoercion = TypeCoercion.Enabled
   arrayOrder = ArrayOrder.Lenient
}

infix fun String.lenientShouldEqualJson(other: String) = this.shouldEqualJson(other, myOptions)

"[1, 2]" lenientShouldEqualJson "[2, 1]" // This will pass
Parameters
Name Purpose Possible values Default
PropertyOrder Determines whether JSON object property order is considered during comparison. PropertyOrder.Strict, PropertyOrder.Lenient PropertyOrder.Lenient; property order does not matter.
ArrayOrder Determines whether JSON array element order is considered during comparison. ArrayOrder.Strict, ArrayOrder.Lenient ArrayOrder.Strict; element order matters.
FieldComparison Determines whether comparison fails when actualJSON contains additional properties compared with expected. FieldComparison.Strict, FieldComparison.Lenient FieldComparison.Strict; additional properties make the values unequal.
NumberFormat Determines whether number comparison is strict about number format, for example whether 100.0 and 100 are considered equal. NumberFormat.Strict, NumberFormat.Lenient NumberFormat.Lenient; number format does not matter.
TypeCoercion Determines whether types are coerced, for example when a string contains a number or boolean value. TypeCoercion.Enabled, TypeCoercion.Disabled TypeCoercion.Disabled; types are not coerced.

Target: Multiplatform

shouldEqualSpecifiedJson

This is an alias for shouldEqualJson, except the default option for FieldComparison is set to FieldComparison.Lenient.

val a = """ { "a": true, "date": "2019-11-03" } """
val b = """ { "a": true } """

// this would pass
a shouldEqualSpecifiedJson b

// this would fail
a shouldEqualJson b

Target: Multiplatform

shouldContainJsonKey

json.shouldContainJsonKey("$.json.path") checks that a JSON string contains the specified JSON path.

The opposite matcher is shouldNotContainJsonKey, which fails if the JSON string contains the specified JSON path.

Target: JVM

shouldContainJsonKeyValue

str.shouldContainJsonKeyValue("$.json.path", value) checks that a JSON string contains a JSON path with the specified value.

The opposite matcher is shouldNotContainJsonKeyValue, which fails if the JSON path contains the specified value.

Target: JVM

shouldMatchJsonResource

json.shouldMatchJsonResource("/file.json") checks that the JSON is the same as the existing test resource /file.json, ignoring property order and formatting.

Target: JVM

JSON Schema Matchers

Matcher Description Target
shouldMatchSchema Checks that a String or kotlinx.serialization.JsonElement matches a JsonSchema. See below for schema configuration. Multiplatform

A subset of JSON Schema can be defined by parsing a text schema.

Example:

val parsedSchema = parseSchema(
  """
  {
  "$id": "https://example.com/geographical-location.schema.json",  // will  be ignored
  "$schema": "https://json-schema.org/draft/2020-12/schema",       // will be ignored
  "title": "Longitude and Latitude Values",                        // will be ignored
  "description": "A geographical coordinate.",                     // will be ignored
  "required": [ "latitude", "longitude" ],
  "type": "object",
  "properties": {
    "latitude": {
      "type": "number",
      "minimum": -90,
      "maximum": 90
    },
    "longitude": {
      "type": "number",
      "minimum": -180,
      "maximum": 180
    }
  }
}
  """
)

Or use Kotest’s built-in DSL:

val addressSchema = jsonSchema {
  obj {   // object is reserved, obj was chosen over jsonObject for brevity but could be changed ofc, or jsonObject could be added as alternative.
    withProperty("street", required = true) { string() }
    withProperty("zipCode", required = true) {
      integer {
        beEven() and beInRange(10000..99999)   // supports constructing a matcher that will be used to test values
      }
    }
    additionalProperties = false   // triggers failure if other properties are defined in actual
  }
}

val personSchema = jsonSchema {
  obj {
    withProperty("name", required = true) { string() }
    withProperty("address") { addressSchema() } // Schemas can re-use other schemas 🎉
  }
}

Building Schemas

Arrays

Arrays are used for ordered elements. In JSON, each element in an array can have a different type.

Length (minItems and maxItems)

Array length can be specified with the minItems and maxItems keywords. Each keyword value must be a non-negative number; the defaults are 0 and Int.MAX_VALUE.

val lengthBoundedSchema = jsonSchema {
  array(minItems = 0, maxItems = 1) { number() }
}
Uniqueness

A schema can check that every item in an array is unique. Just set the uniqueItems keyword to true.

val uniqueArray = jsonSchema {
  array(uniqueItems = true) { number() }
}

⚠️ Kotest currently supports only a subset of JSON Schema. The following items are not currently supported:

  • $defs and $refs
  • Recursive schemas
  • Parsing of schema composition
  • string.format
  • array.prefixItems,
  • array.contains,
  • array.items = false
  • array.maxContains
  • array.minContains
  • array.uniqueItems
  • enum

Validation

Once a schema is defined, it can be used to validate a String or kotlinx.serialization.JsonElement:

"{}" shouldMatchSchema personSchema

// fails with:
// $.name => Expected string, but was undefined

""" { "name": "Emil", "age": 34 } """
// Passes, since address isn't required and `additionalProperties` are allowed

References