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