Skip to content

Working with JSON

The kpointer-kxs module reads and mutates kotlinx.serialization JsonObject and JsonArray values by JSON Pointer. It is built on kpointer-adapter, but for everyday use you work through convenience extensions on the kotlinx.serialization types directly — you rarely touch the adapter layer.

Every mutation produces a new immutable value; the original is unchanged.

One dependency is enough

kpointer-kxs exposes kpointer-adapter and kpointer-core as transitive api dependencies. Declaring kpointer-kxs gives you KPointer, the adapter interfaces, and the JSON extensions all at once. See the Usage Overview for the coordinates.

Reading a value

Use the [] operator with a KPointer, or elementAt with a plain string:

val obj = buildJsonObject {
    putJsonObject("user") {
        put("name", "Alice")
        put("age", 30)
    }
}

val name: JsonElement? = obj[KPointer.from("/user/name")]
println(name)   // "Alice"

// elementAt takes a string and parses the pointer for you
val same: JsonElement? = obj.elementAt("/user/name")

// Absent paths yield null
println(obj.elementAt("/user/email"))   // null

JsonArray works identically, with integer segments addressing elements:

val arr = buildJsonArray {
    addJsonObject { put("name", "Bob") }
    addJsonObject { put("name", "Carol") }
}
val second: JsonElement? = arr[KPointer.from("/1/name")]   // "Carol"

Typed accessors

When you know the shape at a path, the typed accessors return that type directly. They return null for an absent path and throw IllegalArgumentException on a type mismatch — matching the jsonObject / jsonArray / jsonPrimitive idiom of kotlinx.serialization:

val user: JsonObject?    = obj.objectAt("/user")
val tags: JsonArray?     = obj.arrayAt("/user/tags")
val name: JsonPrimitive? = obj.primitiveAt("/user/name")

Each accessor also accepts a pre-parsed KPointer, which avoids re-parsing the string on repeated calls:

val pointer = KPointer.from("/user/name")
val name: JsonPrimitive? = obj.primitiveAt(pointer)

Checking whether a path exists

contains distinguishes "absent" from "present but JsonNull":

KPointer.from("/user/name") in obj                 // true
obj.contains(KPointer.from("/user/nullField"))     // true, even though the value is JsonNull
obj.contains(KPointer.from("/missing"))            // false

Mutating a JsonObject

Call mutate and return the new object. Inside the block, "/path" to value sets (creating or replacing) and remove("/path") deletes:

val updated: JsonObject = obj.mutate {
    "/user/name"   to "Bob"
    "/user/active" to true
    "/user/age"    to 42
    "/user/ratio"  to 3.14
    "/user/temp"   to null
    remove("/user/deprecated")
}

The supported right-hand-side types are String, Boolean, Int, Long, Float, Double, null, and KpaElement. To insert a nested object or array, wrap a JsonElement with .toKpaElement():

val nested = buildJsonObject { put("city", "Springfield") }
val updated = obj.mutate { "/user/address" to nested.toKpaElement() }

A pre-parsed KPointer works as the receiver of to as well:

val pointer = KPointer.from("/user/name")
val updated = obj.mutate { pointer to "Carol" }

Mutating a JsonArray

Integer segments address elements; the RFC 6901 - segment appends:

val original = buildJsonArray { add("a"); add("b"); add("c") }

val replaced: JsonArray = original.mutate { "/1" to "B" }    // ["a", "B", "c"]
val appended: JsonArray = original.mutate { "/-" to "d" }    // ["a", "b", "c", "d"]
val removed:  JsonArray = original.mutate { remove("/1") }   // ["a", "c"]

Nested paths

Both mutate variants navigate arbitrarily deep in a single call:

// Deep set in an object
val obj = buildJsonObject {
    putJsonObject("address") { put("city", "Springfield") }
}.mutate {
    "/address/city" to "Shelbyville"
}

// Deep set in an array
val arr = buildJsonArray {
    addJsonArray { add("x"); add("y") }
}.mutate {
    "/0/1" to "Z"
}
// [["x", "Z"]]

Removing a key that does not exist throws IllegalArgumentException; removing an out-of-bounds array index throws IndexOutOfBoundsException.

Handing adapters to other libraries

If you are integrating with generic code that consumes KpaStruct or KpaList directly, wrap a JsonObject with asKpaStruct() or a JsonArray with asKpaList():

val struct: KpaStruct = obj.asKpaStruct()
someLibrary.consume(struct)

To convert a KpaElement produced by the adapter layer back into a kotlinx.serialization type, call toJsonElement():

val element: JsonElement = someKpaElement.toJsonElement()