Skip to content

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.

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 ~1not /.
"/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).