Skip to content

Repository files navigation

JsonPath4K

A-SIT Plus Official GitHub license Kotlin Kotlin Java Maven Central

This is a Kotlin Multiplatform Library for using Json Paths as specified in RFC9535.

Architecture

This library was built for Kotlin Multiplatform, supporting all KMP targets, except WASI.

Notable features for multiplatform are:

  • Use of Napier for default compiler error logging on JVM/Android, iOS, macOS, tvOS, JS, and WasmJS.
    • By default, no Antilog is registered.
    • On watchOS, Linux, Windows (mingw), and Android Native, the default listener does not log, due to Napier not supporting those platforms; provide a custom error listener if logging is required on those platforms.
  • Use of kotlinx-serialization for serialization from/to JSON and to have JsonElement as evaluation target for JsonPathQuery
  • Use of TestBalloon with TestBalloon Addons for unit tests.

Using the Library

  1. Add JsonPath4K as a dependency in your project (at.asitplus:jsonpath4k:$version)
  2. Use the JsonPath constructor for compiling JSONPath query expressions.
  3. Invoke the method JsonPath.query to select nodes satisfying the JsonPath query expression from a JsonElement.
  4. A nodeList containing both the selected values and their normalized paths is returned.

In general, things are called what they are. Most prominently, JsonPath4K is not used anywhere in code – even though this project is called JsonPath4K, it provides JSONPath functionality for Kotlin Multiplatform, not JsonPath4K functionality because there is no such thing.

val jsonElement = buildJsonArray { add(0) }

val jsonPathQueryExpression = "$[0]"
val jsonPath = JsonPath(jsonPathQueryExpression)

val nodeList = jsonPath.query(jsonElement)
val jsonValue = nodeList[0].value.jsonPrimitive
val normalizedPath = nodeList[0].normalizedJsonPath

Function Extensions

This library supports the function extensions specified in RFC9535 by default.

Custom Function Extensions

Custom function extensions can be added using JsonPath.defaultFunctionExtensionRepository.addExtension:

// adding a logical type function extension with 1 parameter of type NodesType
JsonPath.defaultFunctionExtensionRepository.addExtension("foo") {
    JsonPathFunctionExtension.LogicalTypeFunctionExtension(
        JsonPathFilterExpressionType.ValueType,
        JsonPathFilterExpressionType.LogicalType,
        JsonPathFilterExpressionType.NodesType,
    ) {
        val value0 = it[0] as JsonPathFilterExpressionValue.ValueTypeValue
        val value1 = it[1] as JsonPathFilterExpressionValue.LogicalTypeValue
        val value2 = it[2] as JsonPathFilterExpressionValue.NodesTypeValue
        true
    }
}

// adding a value type function extension returning a JsonValue with 2 parameters of type ValueType
JsonPath.defaultFunctionExtensionRepository.addExtension("foo") {
    JsonPathFunctionExtension.ValueTypeFunctionExtension(
        JsonPathFilterExpressionType.ValueType,
        JsonPathFilterExpressionType.ValueType,
    ) {
        JsonPrimitive("")
    }
}

// adding a logical type function extension with 2 parameters of type ValueType returning false
JsonPath.defaultFunctionExtensionRepository.addExtension("foo") {
    JsonPathFunctionExtension.LogicalTypeFunctionExtension(
        JsonPathFilterExpressionType.ValueType,
        JsonPathFilterExpressionType.ValueType,
    ) {
        false
    }
}

// adding a value type function extension returning the special value `Nothing` with 2 parameters of type LogicalType
JsonPath.defaultFunctionExtensionRepository.addExtension("foo") {
    JsonPathFunctionExtension.ValueTypeFunctionExtension(
        JsonPathFilterExpressionType.LogicalType,
        JsonPathFilterExpressionType.LogicalType,
    ) {
        null
    }
}

// adding a nodes type function extension with 2 parameters of type ValueType
JsonPath.defaultFunctionExtensionRepository.addExtension("foo") {
    JsonPathFunctionExtension.NodesTypeFunctionExtension(
        JsonPathFilterExpressionType.ValueType,
        JsonPathFilterExpressionType.ValueType,
    ) {
        listOf()
    }
}

// reimplementing the count function as defined in [RFC9535](https://www.rfc-editor.org/rfc/rfc9535.html#name-function-extensions)
JsonPath.defaultFunctionExtensionRepository.addExtension("count") {
    JsonPathFunctionExtension.ValueTypeFunctionExtension(
        JsonPathFilterExpressionType.NodesType,
    ) {
        val nodesTypeValue = it[0] as JsonPathFilterExpressionValue.NodesTypeValue
        JsonPrimitive(nodesTypeValue.nodeList.size.toUInt())
    }
}

Removing Function Extensions

Function extensions can be removed from the default repository by setting the value of JsonPath.defaultFunctionExtensionRepository to a new repository.

Existing functions can be preserved by exporting them using JsonPath.defaultFunctionExtensionRepository.export() and selectively importing them into the new repository.

Testing Custom Function Extensions

In order to test custom function extensions without polluting the default function extension repository, it is recommended to make an export and use the resulting map to build a new function extension retriever.

val testRetriever = JsonPath.defaultFunctionExtensionRepository.export().plus(
    "foo" to JsonPathFunctionExtension.LogicalTypeFunctionExtension(
        JsonPathFilterExpressionType.ValueType,
        JsonPathFilterExpressionType.ValueType,
    ) {
        true
    }
)
val jsonPath = JsonPath(jsonPathStatement, functionExtensionRetriever = testRetriever::get)

// select from a json element
jsonPath.query(buildJsonElement {})

Error Handling

Invalid JSONPath expressions fail during JsonPath construction with a JsonPathCompilerException. The compiler's error listener only receives diagnostic details; it does not suppress or replace that exception.

On JVM/Android, iOS, macOS, tvOS, JS, and WasmJS, the default listener sends diagnostics to Napier (provided the application has registered an Antilog). On watchOS, Linux, Windows (mingw), and Android Native, the default listener is a no-op. Compilation failures still throw on these platforms, but logging requires a custom implementation of AntlrJsonPathCompilerErrorListener.

Pass a compiler with that listener to one JsonPath, or assign it as the global default:

val compiler = AntlrJsonPathCompiler(errorListener = myErrorListener)
val jsonPath = JsonPath(jsonPathExpression, compiler = compiler)

// Optional: use it for subsequent JsonPath instances that do not specify a compiler.
JsonPath.defaultCompiler = compiler

Using a single, shared default works because operations only use an ANTLR cache as global state and this cache is guarded to enable concurrent access and mutation.

Contributing

External contributions are greatly appreciated! Just be sure to observe the contribution guidelines (see CONTRIBUTING.md).



The Apache License does not apply to the logos, (including the A-SIT logo) and the project/module name(s), as these are the sole property of A-SIT/A-SIT Plus GmbH and may not be used in derivative works without explicit permission!

About

Kotlin Multiplatform RFC9535 JSONPath Library

Topics

Resources

Contributing

Security policy

Stars

25 stars

Watchers

5 watching

Forks

Releases

Packages

Used by

Contributors

Languages