Creating and Manipulating Pointers¶
The kpointer-core module provides KPointer — an immutable value representing an RFC 6901 JSON
Pointer — and the operations for building, inspecting, and transforming pointers. It has no
document model: a KPointer is just the address, not a value inside any tree. To resolve a pointer
against a document, use one of the adapter modules.
KPointer instances are immutable. You obtain them from a factory function, or from an operation on
an existing KPointer.
Creating a pointer¶
There are three input encodings, each with its own factory, plus a convenience dispatcher.
// Encoding-specific factories
val a = KPointer.fromJsonPointer("/foo/bar") // strict RFC 6901
val b = KPointer.fromFragment("#/foo/bar") // RFC 6901 §6 URI fragment
val c = KPointer.fromDotNotation("foo.bar") // Mustache-style dot notation
// Convenience: from() dispatches on the leading character
// "/…" -> fromJsonPointer
// "#…" -> fromFragment
// otherwise (including "") -> fromDotNotation
val pointer = KPointer.from("/foo/bar") // same as fromJsonPointer
// The root pointer, addressing the whole document
val root = KPointer.ROOT
println(root.isRoot) // true
The empty string is not the root in every encoding
Under RFC 6901, the empty string is the root pointer. Under dot notation, the empty string
is also root. But a leading / is significant: "/" is not root — it is a one-segment
pointer whose single segment is the empty string "". See
Escaping and edge cases.
Inspecting a pointer¶
val pointer = KPointer.from("/foo/bar/baz")
println(pointer.depth) // 3
println(pointer.isRoot) // false
println(pointer[0]) // "foo"
println(pointer[1]) // "bar"
println(pointer[2]) // "baz"
depth is the number of segments; isRoot is equivalent to depth == 0. The indexing operator
returns the decoded segment string.
Navigating and combining¶
val pointer = KPointer.from("/foo/bar/baz")
// Pop the first segment, returning (segment, remainder)
val (segment, remainder) = pointer.pop()
println(segment) // "foo"
println(remainder) // "/bar/baz"
// Parent pointer (all segments except the last)
println(pointer.parent) // "/foo/bar"
// Concatenate two pointers
val a = KPointer.from("/foo")
val b = KPointer.from("/bar")
println(a + b) // "/foo/bar"
Encodings¶
A single pointer can be emitted in any of the three encodings.
RFC 6901¶
The canonical form. Segments are separated by /; ~ and / inside a segment are escaped as ~0
and ~1.
val p = KPointer.from("/a~1b") // one segment: "a/b"
println(p[0]) // "a/b"
println(p.toString()) // "/a~1b" (re-encoded canonically)
URI fragment (RFC 6901 §6)¶
A JSON Pointer can ride inside a URI fragment. fromFragment parses one (it must start with #);
toFragment produces one, percent-encoding characters that are unsafe in a fragment.
val pointer = KPointer.fromFragment("#/foo/bar")
println(pointer.toString()) // "/foo/bar"
val fragment = KPointer.from("/foo/bar").toFragment()
println(fragment) // "#/foo/bar"
// Unsafe characters are percent-encoded
println(KPointer.from("/c%d").toFragment()) // "#/c%25d"
// Round-trips
val original = KPointer.from("/g|h")
println(KPointer.fromFragment(original.toFragment()) == original) // true
Dot notation¶
A kPointer extension (not defined in any RFC), where . separates segments. There is no escape
mechanism, so a segment that contains a literal . cannot be expressed.
val pointer = KPointer.fromDotNotation("foo.bar.baz")
println(pointer) // "/foo/bar/baz"
// A leading '.' is ignored
println(KPointer.fromDotNotation(".foo.bar")) // "/foo/bar"
// Emit dot notation
println(KPointer.fromJsonPointer("/users/0/name").toDotNotation()) // "users.0.name"
// Empty input is root; root emits as "."
println(KPointer.fromDotNotation("").isRoot) // true
println(KPointer.ROOT.toDotNotation()) // "."
Empty segments are rejected when parsing ("foo.", "foo..bar", "..foo" all throw
IllegalArgumentException). Segments containing . are rejected when emitting. Unlike RFC 6901,
/ and ~ are not special in dot notation — they appear verbatim.
Relative JSON Pointers¶
draft-bhutton-relative-json-pointer-00
defines relative pointers — strings that navigate relative to a known base pointer. kPointer
exposes them through operators on KPointer.
Use + to apply a relative pointer string to a base. The result is a RelativePointerResult, one
of Pointer, Index, or Key:
val base = KPointer.from("/foo/1")
when (val result = base + "1/0") {
is RelativePointerResult.Pointer -> println(result.pointer) // "/foo/0"
is RelativePointerResult.Index -> println(result.index)
is RelativePointerResult.Key -> println(result.key)
}
// Ascend to the root
println((base + "2") as RelativePointerResult.Pointer) // Pointer("")
// '#' suffix: yield the array index or object key of the resolved location
val idx = base + "0#" // RelativePointerResult.Index(1) — "1" is an integer segment
val key = base + "1#" // RelativePointerResult.Key("foo") — "foo" is a string segment
Use - to compute the relative pointer from one pointer to another. The result always uses the
minimal "go-up-N, then descend" form (never an index adjustment, never a trailing #):
val a = KPointer.from("/foo/bar")
val b = KPointer.from("/foo/baz")
val rel = a - b // "1/baz"
val roundTrip = a + rel // RelativePointerResult.Pointer("/foo/baz")
Parsing a relative pointer with fromJsonPointer or fromFragment throws
IllegalArgumentException; the from dispatcher routes such strings to fromDotNotation instead.
Escaping and edge cases¶
RFC 6901 has a few corners that are easy to get wrong. kPointer handles them per the spec:
| Input | Segments | Note |
|---|---|---|
"" |
(none) | Root pointer, depth == 0. |
"/" |
[""] |
One segment, the empty string — not root. |
"/foo/" |
["foo", ""] |
A trailing slash adds an empty-string segment. |
"/a~1b" |
["a/b"] |
~1 decodes to /. |
"/a~0b" |
["a~b"] |
~0 decodes to ~. |
"/a~01b" |
["a~1b"] |
Decode order matters: ~0→~ first, then the literal 1, giving ~1 — not /. |
"/0" |
["0"] |
A numeric-looking segment is just the string "0"; whether it means "index 0" or "key 0" is decided by the document at resolution time, not by the pointer. |
The last two rows are the classic landmines. ~01 must be decoded left to right as escape sequences
(~0 → ~), not by first expanding ~1 → /; doing the latter would wrongly yield /. And a
numeric segment carries no array-vs-object intent on its own — a KPointer is purely syntactic.