RFC6901 JSON Pointer implementation in OCaml using jsont
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

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`