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.fromKPointer("/foo/bar") // strict RFC 6901
val b = KPointer.fromFragment("#/foo/bar") // RFC 6901 §6 URI fragment
val c = KPointer.fromJsPath("foo.bar") // JavaScript access path
// Convenience: from() dispatches on the leading character
// "/…" -> fromKPointer
// "#…" -> fromFragment
// otherwise (including "") -> fromJsPath
val pointer = KPointer.from("/foo/bar") // same as fromKPointer
// 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 the JavaScript access path
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
JavaScript access path¶
A kPointer extension (not defined in any RFC) — the static member-access subset of JavaScript used
by tools like Vega-Lite's encoding.*.field accessors: dot-separated segments, ['…']/["…"]
quoted-bracket segments, [N] numeric-bracket segments, and backslash escaping (\., \[, \])
for a literal ., [, or ] inside a dot segment. Unlike dot notation, a bracket is structural: a
numeric bracket segment array-indexes, and a quoted-bracket segment can hold any literal string,
including one containing ..
val pointer = KPointer.fromJsPath("foo.bar.baz")
println(pointer) // "/foo/bar/baz"
// Brackets are structural, not literal
println(KPointer.fromJsPath("foo[0].bar")) // "/foo/0/bar"
println(KPointer.fromJsPath("foo['a.b']")) // one segment "a.b" under "foo"
// Emit the JavaScript access path
println(KPointer.fromKPointer("/users/0/name").toJsPath()) // "users[0].name"
// Empty input is root; root emits as ""
println(KPointer.fromJsPath("").isRoot) // true
println(KPointer.ROOT.toJsPath()) // ""
toJsPath() is dot-preferred and lossless — it never throws: a numeric segment is emitted in
bracket form ([0]); a segment safe to write as a bare dot identifier (non-empty, containing none
of ., [, ], ', ", or \) is emitted as .key; any other segment is bracket-quoted
(['…']), with ' and \ backslash-escaped. fromJsPath(p.toJsPath()) == p holds for every
pointer, including KPointer.ROOT. A leading . is rejected (an empty first segment), and
foo., foo..bar are rejected the same way — but a leading bracket is allowed, since it is needed
for round-tripping a pointer whose first segment is numeric or otherwise bracket-quoted.
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 fromKPointer or fromFragment throws
IllegalArgumentException; the from dispatcher routes such strings to fromJsPath 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 /. |
"/a~2b", "/a~" |
(throws) | ~ must be immediately followed by 0 or 1; any other following character — or none, at the end of a segment — is rejected. |
"/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 three 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
/. An invalid ~ escape is rejected during parsing, before any document is involved. And a
numeric segment carries no array-vs-object intent on its own — a KPointer is purely syntactic;
whether "0" is treated as an array index is decided when an adapter resolves it against a
document, using the RFC 6902 array-index grammar ("0", or a non-zero ASCII digit followed by
ASCII digits — no leading zeros, no non-ASCII digits, no sign).