RFC6901 JSON Pointer implementation in OCaml using jsont
at main 27 kB view raw
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}