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.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.

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