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