RFC6901 JSON Pointer implementation in OCaml using jsont
at 239479e64faceacf2c1621af0c6dd702beccbd03 827 lines 26 kB view raw
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>