RFC6901 JSON Pointer implementation in OCaml using jsont
1# JSON Pointer Tutorial
2
3This tutorial introduces JSON Pointer as defined in
4[RFC 6901](https://www.rfc-editor.org/rfc/rfc6901), and demonstrates
5the `jsont-pointer` OCaml library through interactive examples.
6
7## JSON Pointer vs JSON Path
8
9Before diving in, it's worth understanding the difference between JSON
10Pointer and JSON Path, as they serve different purposes:
11
12**JSON Pointer** (RFC 6901) is an *indicator syntax* that specifies a
13*single location* within JSON data. It always identifies at most one
14value.
15
16**JSON Path** is a *query syntax* that can *search* JSON data and return
17*multiple* values matching specified criteria.
18
19Use JSON Pointer when you need to address a single, specific location
20(like JSON Schema's `$ref`). Use JSON Path when you might need multiple
21results (like Kubernetes queries).
22
23The `jsont-pointer` library implements JSON Pointer and integrates with
24the `Jsont.Path` type for representing navigation indices.
25
26## Setup
27
28First, let's set up our environment with helper functions:
29
30```ocaml
31# open Jsont_pointer;;
32# #install_printer Jsont_pointer_top.nav_printer;;
33# #install_printer Jsont_pointer_top.append_printer;;
34# #install_printer Jsont_pointer_top.json_printer;;
35# #install_printer Jsont_pointer_top.error_printer;;
36# let parse_json s =
37 match Jsont_bytesrw.decode_string Jsont.json s with
38 | Ok json -> json
39 | Error e -> failwith e;;
40val parse_json : string -> Jsont.json = <fun>
41```
42
43## What is JSON Pointer?
44
45From RFC 6901, Section 1:
46
47> JSON Pointer defines a string syntax for identifying a specific value
48> within a JavaScript Object Notation (JSON) document.
49
50In other words, JSON Pointer is an addressing scheme for locating values
51inside a JSON structure. Think of it like a filesystem path, but for JSON
52documents instead of files.
53
54For example, given this JSON document:
55
56```ocaml
57# let users_json = parse_json {|{
58 "users": [
59 {"name": "Alice", "age": 30},
60 {"name": "Bob", "age": 25}
61 ]
62 }|};;
63val users_json : Jsont.json =
64 {"users":[{"name":"Alice","age":30},{"name":"Bob","age":25}]}
65```
66
67The JSON Pointer `/users/0/name` refers to the string `"Alice"`:
68
69```ocaml
70# let ptr = of_string_nav "/users/0/name";;
71val ptr : nav t = [Mem "users"; Nth 0; Mem "name"]
72# get ptr users_json;;
73- : Jsont.json = "Alice"
74```
75
76In OCaml, this is represented by the `'a Jsont_pointer.t` type - a sequence
77of navigation steps from the document root to a target value. The phantom
78type parameter `'a` encodes whether this is a navigation pointer or an
79append pointer (more on this later).
80
81## Syntax: Reference Tokens
82
83RFC 6901, Section 3 defines the syntax:
84
85> A JSON Pointer is a Unicode string containing a sequence of zero or more
86> reference tokens, each prefixed by a '/' (%x2F) character.
87
88The grammar is elegantly simple:
89
90```
91json-pointer = *( "/" reference-token )
92reference-token = *( unescaped / escaped )
93```
94
95This means:
96- The empty string `""` is a valid pointer (it refers to the whole document)
97- Every non-empty pointer starts with `/`
98- Everything between `/` characters is a "reference token"
99
100Let's see this in action:
101
102```ocaml
103# of_string_nav "";;
104- : nav t = []
105```
106
107The empty pointer has no reference tokens - it points to the root.
108
109```ocaml
110# of_string_nav "/foo";;
111- : nav t = [Mem "foo"]
112```
113
114The pointer `/foo` has one token: `foo`. Since it's not a number, it's
115interpreted as an object member name (`Mem`).
116
117```ocaml
118# of_string_nav "/foo/0";;
119- : nav t = [Mem "foo"; Nth 0]
120```
121
122Here we have two tokens: `foo` (a member name) and `0` (interpreted as
123an array index `Nth`).
124
125```ocaml
126# of_string_nav "/foo/bar/baz";;
127- : nav t = [Mem "foo"; Mem "bar"; Mem "baz"]
128```
129
130Multiple tokens navigate deeper into nested structures.
131
132### The Index Type
133
134Each reference token is represented using `Jsont.Path.index`:
135
136<!-- $MDX skip -->
137```ocaml
138type index = Jsont.Path.index
139(* = Jsont.Path.Mem of string * Jsont.Meta.t
140 | Jsont.Path.Nth of int * Jsont.Meta.t *)
141```
142
143The `Mem` constructor is for object member access, and `Nth` is for array
144index access. The member name is **unescaped** - you work with the actual
145key string (like `"a/b"`) and the library handles any escaping needed
146for the JSON Pointer string representation.
147
148### Invalid Syntax
149
150What happens if a pointer doesn't start with `/`?
151
152```ocaml
153# of_string_nav "foo";;
154Exception:
155Jsont.Error Invalid JSON Pointer: must be empty or start with '/': foo.
156```
157
158The RFC is strict: non-empty pointers MUST start with `/`.
159
160For safer parsing, use `of_string_result`:
161
162```ocaml
163# of_string_result "foo";;
164- : ([ `Append of append t | `Nav of nav t ], string) result =
165Error "Invalid JSON Pointer: must be empty or start with '/': foo"
166# of_string_result "/valid";;
167- : ([ `Append of append t | `Nav of nav t ], string) result =
168Ok (`Nav [Mem "valid"])
169```
170
171## Evaluation: Navigating JSON
172
173Now we come to the heart of JSON Pointer: evaluation. RFC 6901, Section 4
174describes how a pointer is resolved against a JSON document:
175
176> Evaluation of a JSON Pointer begins with a reference to the root value
177> of a JSON document and completes with a reference to some value within
178> the document. Each reference token in the JSON Pointer is evaluated
179> sequentially.
180
181Let's use the example JSON document from RFC 6901, Section 5:
182
183```ocaml
184# let rfc_example = parse_json {|{
185 "foo": ["bar", "baz"],
186 "": 0,
187 "a/b": 1,
188 "c%d": 2,
189 "e^f": 3,
190 "g|h": 4,
191 "i\\j": 5,
192 "k\"l": 6,
193 " ": 7,
194 "m~n": 8
195 }|};;
196val rfc_example : Jsont.json =
197 {"foo":["bar","baz"],"":0,"a/b":1,"c%d":2,"e^f":3,"g|h":4,"i\\j":5,"k\"l":6," ":7,"m~n":8}
198```
199
200This document is carefully constructed to exercise various edge cases!
201
202### The Root Pointer
203
204```ocaml
205# get root rfc_example ;;
206- : Jsont.json =
207{"foo":["bar","baz"],"":0,"a/b":1,"c%d":2,"e^f":3,"g|h":4,"i\\j":5,"k\"l":6," ":7,"m~n":8}
208```
209
210The empty pointer (`root`) returns the whole document.
211
212### Object Member Access
213
214```ocaml
215# get (of_string_nav "/foo") rfc_example ;;
216- : Jsont.json = ["bar","baz"]
217```
218
219`/foo` accesses the member named `foo`, which is an array.
220
221### Array Index Access
222
223```ocaml
224# get (of_string_nav "/foo/0") rfc_example ;;
225- : Jsont.json = "bar"
226# get (of_string_nav "/foo/1") rfc_example ;;
227- : Jsont.json = "baz"
228```
229
230`/foo/0` first goes to `foo`, then accesses index 0 of the array.
231
232### Empty String as Key
233
234JSON allows empty strings as object keys:
235
236```ocaml
237# get (of_string_nav "/") rfc_example ;;
238- : Jsont.json = 0
239```
240
241The pointer `/` has one token: the empty string. This accesses the member
242with an empty name.
243
244### Keys with Special Characters
245
246The RFC example includes keys with `/` and `~` characters:
247
248```ocaml
249# get (of_string_nav "/a~1b") rfc_example ;;
250- : Jsont.json = 1
251```
252
253The token `a~1b` refers to the key `a/b`. We'll explain this escaping
254[below](#escaping-special-characters).
255
256```ocaml
257# get (of_string_nav "/m~0n") rfc_example ;;
258- : Jsont.json = 8
259```
260
261The token `m~0n` refers to the key `m~n`.
262
263**Important**: When using the OCaml library programmatically, you don't need
264to worry about escaping. The `Mem` variant holds the literal key name:
265
266```ocaml
267# let slash_ptr = make [mem "a/b"];;
268val slash_ptr : nav t = [Mem "a/b"]
269# to_string slash_ptr;;
270- : string = "/a~1b"
271# get slash_ptr rfc_example ;;
272- : Jsont.json = 1
273```
274
275The library escapes it when converting to string.
276
277### Other Special Characters (No Escaping Needed)
278
279Most characters don't need escaping in JSON Pointer strings:
280
281```ocaml
282# get (of_string_nav "/c%d") rfc_example ;;
283- : Jsont.json = 2
284# get (of_string_nav "/e^f") rfc_example ;;
285- : Jsont.json = 3
286# get (of_string_nav "/g|h") rfc_example ;;
287- : Jsont.json = 4
288# get (of_string_nav "/ ") rfc_example ;;
289- : Jsont.json = 7
290```
291
292Even a space is a valid key character!
293
294### Error Conditions
295
296What happens when we try to access something that doesn't exist?
297
298```ocaml
299# get_result (of_string_nav "/nonexistent") rfc_example;;
300- : (Jsont.json, Jsont.Error.t) result =
301Error JSON Pointer: member 'nonexistent' not found
302File "-":
303# find (of_string_nav "/nonexistent") rfc_example;;
304- : Jsont.json option = None
305```
306
307Or an out-of-bounds array index:
308
309```ocaml
310# find (of_string_nav "/foo/99") rfc_example;;
311- : Jsont.json option = None
312```
313
314Or try to index into a non-container:
315
316```ocaml
317# find (of_string_nav "/foo/0/invalid") rfc_example;;
318- : Jsont.json option = None
319```
320
321The library provides both exception-raising and result-returning variants:
322
323<!-- $MDX skip -->
324```ocaml
325val get : nav t -> Jsont.json -> Jsont.json
326val get_result : nav t -> Jsont.json -> (Jsont.json, Jsont.Error.t) result
327val find : nav t -> Jsont.json -> Jsont.json option
328```
329
330### Array Index Rules
331
332RFC 6901 has specific rules for array indices. Section 4 states:
333
334> characters comprised of digits [...] that represent an unsigned base-10
335> integer value, making the new referenced value the array element with
336> the zero-based index identified by the token
337
338And importantly:
339
340> note that leading zeros are not allowed
341
342```ocaml
343# of_string_nav "/foo/0";;
344- : nav t = [Mem "foo"; Nth 0]
345```
346
347Zero itself is fine.
348
349```ocaml
350# of_string_nav "/foo/01";;
351- : nav t = [Mem "foo"; Mem "01"]
352```
353
354But `01` has a leading zero, so it's NOT treated as an array index - it
355becomes a member name instead. This protects against accidental octal
356interpretation.
357
358## The End-of-Array Marker: `-` and Type Safety
359
360RFC 6901, Section 4 introduces a special token:
361
362> exactly the single character "-", making the new referenced value the
363> (nonexistent) member after the last array element.
364
365This `-` marker is unique to JSON Pointer (JSON Path has no equivalent).
366It's primarily useful for JSON Patch operations (RFC 6902) to append
367elements to arrays.
368
369### Navigation vs Append Pointers
370
371The `jsont-pointer` library uses **phantom types** to encode the difference
372between pointers that can be used for navigation and pointers that target
373the "append position":
374
375<!-- $MDX skip -->
376```ocaml
377type nav (* A pointer to an existing element *)
378type append (* A pointer ending with "-" (append position) *)
379type 'a t (* Pointer with phantom type parameter *)
380```
381
382When you parse a pointer, you get either a `nav t` or an `append t`:
383
384```ocaml
385# of_string "/foo/0";;
386- : [ `Append of append t | `Nav of nav t ] = `Nav [Mem "foo"; Nth 0]
387# of_string "/foo/-";;
388- : [ `Append of append t | `Nav of nav t ] = `Append [Mem "foo"] /-
389```
390
391The `-` creates an `append` pointer. Note that in the internal
392representation, the append position is tracked separately (shown as `/-`).
393
394### Why Phantom Types?
395
396The RFC explains that `-` refers to a *nonexistent* position:
397
398> Note that the use of the "-" character to index an array will always
399> result in such an error condition because by definition it refers to
400> a nonexistent array element.
401
402So you **cannot use `get` or `find`** with an append pointer - it makes
403no sense to retrieve a value from a position that doesn't exist! The
404library enforces this at compile time:
405
406```ocaml
407# (* This won't compile: get requires nav t, not append t *)
408 (* get (match of_string "/foo/-" with `Append p -> p | _ -> assert false) rfc_example;; *)
409```
410
411However, append pointers **are** valid for mutation operations like `add`:
412
413```ocaml
414# let arr_obj = parse_json {|{"foo":["a","b"]}|};;
415val arr_obj : Jsont.json = {"foo":["a","b"]}
416# match of_string "/foo/-" with
417 | `Append p -> add p arr_obj ~value:(Jsont.Json.string "c")
418 | `Nav _ -> assert false ;;
419- : Jsont.json = {"foo":["a","b","c"]}
420```
421
422For convenience, use `of_string_nav` when you know a pointer shouldn't
423contain `-`:
424
425```ocaml
426# of_string_nav "/foo/0";;
427- : nav t = [Mem "foo"; Nth 0]
428# of_string_nav "/foo/-";;
429Exception:
430Jsont.Error Invalid JSON Pointer: '-' not allowed in navigation pointer.
431```
432
433### Creating Append Pointers Programmatically
434
435You can convert a navigation pointer to an append pointer using `at_end`:
436
437```ocaml
438# let nav_ptr = of_string_nav "/foo";;
439val nav_ptr : nav t = [Mem "foo"]
440# let app_ptr = at_end nav_ptr;;
441val app_ptr : append t = [Mem "foo"] /-
442# to_string app_ptr;;
443- : string = "/foo/-"
444```
445
446## Mutation Operations
447
448While RFC 6901 defines JSON Pointer for read-only access, RFC 6902
449(JSON Patch) uses JSON Pointer for modifications. The `jsont-pointer`
450library provides these operations.
451
452### Which Pointer Type for Which Operation?
453
454The phantom type system enforces correct usage:
455
456| Operation | Accepts | Because |
457|-----------|---------|---------|
458| `get`, `find` | `nav t` only | Can't retrieve from non-existent position |
459| `remove` | `nav t` only | Can't remove what doesn't exist |
460| `replace` | `nav t` only | Can't replace what doesn't exist |
461| `test` | `nav t` only | Can't test non-existent position |
462| `add` | `_ t` (both) | Can add at existing position OR append |
463| `set` | `_ t` (both) | Can set existing position OR append |
464| `move`, `copy` | `from:nav t`, `path:_ t` | Source must exist, dest can be append |
465
466### Add
467
468The `add` operation inserts a value at a location:
469
470```ocaml
471# let obj = parse_json {|{"foo":"bar"}|};;
472val obj : Jsont.json = {"foo":"bar"}
473# add (of_string_nav "/baz") obj ~value:(Jsont.Json.string "qux")
474 ;;
475- : Jsont.json = {"foo":"bar","baz":"qux"}
476```
477
478For arrays, `add` inserts BEFORE the specified index:
479
480```ocaml
481# let arr_obj = parse_json {|{"foo":["a","b"]}|};;
482val arr_obj : Jsont.json = {"foo":["a","b"]}
483# add (of_string_nav "/foo/1") arr_obj ~value:(Jsont.Json.string "X")
484 ;;
485- : Jsont.json = {"foo":["a","X","b"]}
486```
487
488This is where the `-` marker and append pointers shine - they append to the end:
489
490```ocaml
491# match of_string "/foo/-" with
492 | `Append p -> add p arr_obj ~value:(Jsont.Json.string "c")
493 | `Nav _ -> assert false ;;
494- : Jsont.json = {"foo":["a","b","c"]}
495```
496
497Or more conveniently using `at_end`:
498
499```ocaml
500# add (at_end (of_string_nav "/foo")) arr_obj ~value:(Jsont.Json.string "c")
501 ;;
502- : Jsont.json = {"foo":["a","b","c"]}
503```
504
505### Remove
506
507The `remove` operation deletes a value. It only accepts `nav t` because
508you can only remove something that exists:
509
510```ocaml
511# let two_fields = parse_json {|{"foo":"bar","baz":"qux"}|};;
512val two_fields : Jsont.json = {"foo":"bar","baz":"qux"}
513# remove (of_string_nav "/baz") two_fields ;;
514- : Jsont.json = {"foo":"bar"}
515```
516
517For arrays, it removes and shifts:
518
519```ocaml
520# let three_elem = parse_json {|{"foo":["a","b","c"]}|};;
521val three_elem : Jsont.json = {"foo":["a","b","c"]}
522# remove (of_string_nav "/foo/1") three_elem ;;
523- : Jsont.json = {"foo":["a","c"]}
524```
525
526### Replace
527
528The `replace` operation updates an existing value:
529
530```ocaml
531# replace (of_string_nav "/foo") obj ~value:(Jsont.Json.string "baz")
532 ;;
533- : Jsont.json = {"foo":"baz"}
534```
535
536Unlike `add`, `replace` requires the target to already exist (hence `nav t`).
537Attempting to replace a nonexistent path raises an error.
538
539### Move
540
541The `move` operation relocates a value. The source (`from`) must be a `nav t`
542(you can only move something that exists), but the destination (`path`) can
543be either:
544
545```ocaml
546# let nested = parse_json {|{"foo":{"bar":"baz"},"qux":{}}|};;
547val nested : Jsont.json = {"foo":{"bar":"baz"},"qux":{}}
548# move ~from:(of_string_nav "/foo/bar") ~path:(of_string_nav "/qux/thud") nested
549 ;;
550- : Jsont.json = {"foo":{},"qux":{"thud":"baz"}}
551```
552
553### Copy
554
555The `copy` operation duplicates a value (same typing as `move`):
556
557```ocaml
558# let to_copy = parse_json {|{"foo":{"bar":"baz"}}|};;
559val to_copy : Jsont.json = {"foo":{"bar":"baz"}}
560# copy ~from:(of_string_nav "/foo/bar") ~path:(of_string_nav "/foo/qux") to_copy
561 ;;
562- : Jsont.json = {"foo":{"bar":"baz","qux":"baz"}}
563```
564
565### Test
566
567The `test` operation verifies a value (useful in JSON Patch):
568
569```ocaml
570# test (of_string_nav "/foo") obj ~expected:(Jsont.Json.string "bar");;
571- : bool = true
572# test (of_string_nav "/foo") obj ~expected:(Jsont.Json.string "wrong");;
573- : bool = false
574```
575
576## Escaping Special Characters
577
578RFC 6901, Section 3 explains the escaping rules:
579
580> Because the characters '\~' (%x7E) and '/' (%x2F) have special meanings
581> in JSON Pointer, '\~' needs to be encoded as '\~0' and '/' needs to be
582> encoded as '\~1' when these characters appear in a reference token.
583
584Why these specific characters?
585- `/` separates tokens, so it must be escaped inside a token
586- `~` is the escape character itself, so it must also be escaped
587
588The escape sequences are:
589- `~0` represents `~` (tilde)
590- `~1` represents `/` (forward slash)
591
592### The Library Handles Escaping Automatically
593
594**Important**: When using `jsont-pointer` programmatically, you rarely need
595to think about escaping. The `Mem` variant stores unescaped strings,
596and escaping happens automatically during serialization:
597
598```ocaml
599# let p = make [mem "a/b"];;
600val p : nav t = [Mem "a/b"]
601# to_string p;;
602- : string = "/a~1b"
603# of_string_nav "/a~1b";;
604- : nav t = [Mem "a/b"]
605```
606
607### Escaping in Action
608
609The `Token` module exposes the escaping functions:
610
611```ocaml
612# Token.escape "hello";;
613- : string = "hello"
614# Token.escape "a/b";;
615- : string = "a~1b"
616# Token.escape "a~b";;
617- : string = "a~0b"
618# Token.escape "~/";;
619- : string = "~0~1"
620```
621
622### Unescaping
623
624And the reverse process:
625
626```ocaml
627# Token.unescape "a~1b";;
628- : string = "a/b"
629# Token.unescape "a~0b";;
630- : string = "a~b"
631```
632
633### The Order Matters!
634
635RFC 6901, Section 4 is careful to specify the unescaping order:
636
637> Evaluation of each reference token begins by decoding any escaped
638> character sequence. This is performed by first transforming any
639> occurrence of the sequence '~1' to '/', and then transforming any
640> occurrence of the sequence '~0' to '~'. By performing the substitutions
641> in this order, an implementation avoids the error of turning '~01' first
642> into '~1' and then into '/', which would be incorrect (the string '~01'
643> correctly becomes '~1' after transformation).
644
645Let's verify this tricky case:
646
647```ocaml
648# Token.unescape "~01";;
649- : string = "~1"
650```
651
652If we unescaped `~0` first, `~01` would become `~1`, which would then become
653`/`. But that's wrong! The sequence `~01` should become the literal string
654`~1` (a tilde followed by the digit one).
655
656## URI Fragment Encoding
657
658JSON Pointers can be embedded in URIs. RFC 6901, Section 6 explains:
659
660> A JSON Pointer can be represented in a URI fragment identifier by
661> encoding it into octets using UTF-8, while percent-encoding those
662> characters not allowed by the fragment rule in RFC 3986.
663
664This adds percent-encoding on top of the `~0`/`~1` escaping:
665
666```ocaml
667# to_uri_fragment (of_string_nav "/foo");;
668- : string = "/foo"
669# to_uri_fragment (of_string_nav "/a~1b");;
670- : string = "/a~1b"
671# to_uri_fragment (of_string_nav "/c%d");;
672- : string = "/c%25d"
673# to_uri_fragment (of_string_nav "/ ");;
674- : string = "/%20"
675```
676
677The `%` character must be percent-encoded as `%25` in URIs, and
678spaces become `%20`.
679
680Here's the RFC example showing the URI fragment forms:
681
682| JSON Pointer | URI Fragment | Value |
683|-------------|-------------|-------|
684| `""` | `#` | whole document |
685| `"/foo"` | `#/foo` | `["bar", "baz"]` |
686| `"/foo/0"` | `#/foo/0` | `"bar"` |
687| `"/"` | `#/` | `0` |
688| `"/a~1b"` | `#/a~1b` | `1` |
689| `"/c%d"` | `#/c%25d` | `2` |
690| `"/ "` | `#/%20` | `7` |
691| `"/m~0n"` | `#/m~0n` | `8` |
692
693## Building Pointers Programmatically
694
695Instead of parsing strings, you can build pointers from indices:
696
697```ocaml
698# let port_ptr = make [mem "database"; mem "port"];;
699val port_ptr : nav t = [Mem "database"; Mem "port"]
700# to_string port_ptr;;
701- : string = "/database/port"
702```
703
704For array access, use the `nth` helper:
705
706```ocaml
707# let first_feature_ptr = make [mem "features"; nth 0];;
708val first_feature_ptr : nav t = [Mem "features"; Nth 0]
709# to_string first_feature_ptr;;
710- : string = "/features/0"
711```
712
713### Pointer Navigation
714
715You can build pointers incrementally using the `/` operator (or `append_index`):
716
717```ocaml
718# let db_ptr = of_string_nav "/database";;
719val db_ptr : nav t = [Mem "database"]
720# let creds_ptr = db_ptr / mem "credentials";;
721val creds_ptr : nav t = [Mem "database"; Mem "credentials"]
722# let user_ptr = creds_ptr / mem "username";;
723val user_ptr : nav t = [Mem "database"; Mem "credentials"; Mem "username"]
724# to_string user_ptr;;
725- : string = "/database/credentials/username"
726```
727
728Or concatenate two pointers:
729
730```ocaml
731# let base = of_string_nav "/api/v1";;
732val base : nav t = [Mem "api"; Mem "v1"]
733# let endpoint = of_string_nav "/users/0";;
734val endpoint : nav t = [Mem "users"; Nth 0]
735# to_string (concat base endpoint);;
736- : string = "/api/v1/users/0"
737```
738
739## Jsont Integration
740
741The library integrates with the `Jsont` codec system, allowing you to
742combine JSON Pointer navigation with typed decoding. This is powerful
743because you can point to a location in a JSON document and decode it
744directly to an OCaml type.
745
746```ocaml
747# let config_json = parse_json {|{
748 "database": {
749 "host": "localhost",
750 "port": 5432,
751 "credentials": {"username": "admin", "password": "secret"}
752 },
753 "features": ["auth", "logging", "metrics"]
754 }|};;
755val config_json : Jsont.json =
756 {"database":{"host":"localhost","port":5432,"credentials":{"username":"admin","password":"secret"}},"features":["auth","logging","metrics"]}
757```
758
759### Typed Access with `path`
760
761The `path` combinator combines pointer navigation with typed decoding:
762
763```ocaml
764# let db_host =
765 Jsont.Json.decode
766 (path (of_string_nav "/database/host") Jsont.string)
767 config_json
768 |> Result.get_ok;;
769val db_host : string = "localhost"
770# let db_port =
771 Jsont.Json.decode
772 (path (of_string_nav "/database/port") Jsont.int)
773 config_json
774 |> Result.get_ok;;
775val db_port : int = 5432
776```
777
778Extract a list of strings:
779
780```ocaml
781# let features =
782 Jsont.Json.decode
783 (path (of_string_nav "/features") Jsont.(list string))
784 config_json
785 |> Result.get_ok;;
786val features : string list = ["auth"; "logging"; "metrics"]
787```
788
789### Default Values with `~absent`
790
791Use `~absent` to provide a default when a path doesn't exist:
792
793```ocaml
794# let timeout =
795 Jsont.Json.decode
796 (path ~absent:30 (of_string_nav "/database/timeout") Jsont.int)
797 config_json
798 |> Result.get_ok;;
799val timeout : int = 30
800```
801
802### Nested Path Extraction
803
804You can extract values from deeply nested structures:
805
806```ocaml
807# let org_json = parse_json {|{
808 "organization": {
809 "owner": {"name": "Alice", "email": "alice@example.com", "age": 35},
810 "members": [{"name": "Bob", "email": "bob@example.com", "age": 28}]
811 }
812 }|};;
813val org_json : Jsont.json =
814 {"organization":{"owner":{"name":"Alice","email":"alice@example.com","age":35},"members":[{"name":"Bob","email":"bob@example.com","age":28}]}}
815# Jsont.Json.decode
816 (path (of_string_nav "/organization/owner/name") Jsont.string)
817 org_json
818 |> Result.get_ok;;
819- : string = "Alice"
820# Jsont.Json.decode
821 (path (of_string_nav "/organization/members/0/age") Jsont.int)
822 org_json
823 |> Result.get_ok;;
824- : int = 28
825```
826
827### Comparison: Raw vs Typed Access
828
829**Raw access** requires pattern matching:
830
831```ocaml
832# let raw_port =
833 match get (of_string_nav "/database/port") config_json with
834 | Jsont.Number (f, _) -> int_of_float f
835 | _ -> failwith "expected number";;
836val raw_port : int = 5432
837```
838
839**Typed access** is cleaner and type-safe:
840
841```ocaml
842# let typed_port =
843 Jsont.Json.decode
844 (path (of_string_nav "/database/port") Jsont.int)
845 config_json
846 |> Result.get_ok;;
847val typed_port : int = 5432
848```
849
850The typed approach catches mismatches at decode time with clear errors.
851
852## Summary
853
854JSON Pointer (RFC 6901) provides a simple but powerful way to address
855values within JSON documents:
856
8571. **Syntax**: Pointers are strings of `/`-separated reference tokens
8582. **Escaping**: Use `~0` for `~` and `~1` for `/` in tokens (handled automatically by the library)
8593. **Evaluation**: Tokens navigate through objects (by key) and arrays (by index)
8604. **URI Encoding**: Pointers can be percent-encoded for use in URIs
8615. **Mutations**: Combined with JSON Patch (RFC 6902), pointers enable structured updates
8626. **Type Safety**: Phantom types (`nav t` vs `append t`) prevent misuse of append pointers with retrieval operations
863
864The `jsont-pointer` library implements all of this with type-safe OCaml
865interfaces, integration with the `jsont` codec system, and proper error
866handling for malformed pointers and missing values.
867
868### Key Points on JSON Pointer vs JSON Path
869
870- **JSON Pointer** addresses a *single* location (like a file path)
871- **JSON Path** queries for *multiple* values (like a search)
872- The `-` token is unique to JSON Pointer - it means "append position" for arrays
873- The library uses phantom types to enforce that `-` (append) pointers cannot be used with `get`/`find`