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:
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:
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():
To convert a KpaElement produced by the adapter layer back into a kotlinx.serialization type, call
toJsonElement():