RFC6901 JSON Pointer implementation in OCaml using jsont

odoc

+19 -19
doc/index.html
··· 50 "k\"l": 6, 51 " ": 7, 52 "m~n": 8 53 - }|};;</x-ocaml></div><p>This document is carefully constructed to exercise various edge cases!</p><h3 id="the-root-pointer"><a href="#the-root-pointer" class="anchor"></a>The Root Pointer</h3><div class="x-ocaml-wrapper"><x-ocaml>get root rfc_example ;;</x-ocaml></div><p>The empty pointer (<code>root</code>) returns the whole document.</p><h3 id="object-member-access"><a href="#object-member-access" class="anchor"></a>Object Member Access</h3><div class="x-ocaml-wrapper"><x-ocaml>get (of_string_nav "/foo") rfc_example ;;</x-ocaml></div><p><code>/foo</code> accesses the member named <code>foo</code>, which is an array.</p><h3 id="array-index-access"><a href="#array-index-access" class="anchor"></a>Array Index Access</h3><div class="x-ocaml-wrapper"><x-ocaml>get (of_string_nav "/foo/0") rfc_example ;; 54 get (of_string_nav "/foo/1") rfc_example ;;</x-ocaml></div><p><code>/foo/0</code> first goes to <code>foo</code>, then accesses index 0 of the array.</p><h3 id="empty-string-as-key"><a href="#empty-string-as-key" class="anchor"></a>Empty String as Key</h3><p>JSON allows empty strings as object keys:</p><div class="x-ocaml-wrapper"><x-ocaml>get (of_string_nav "/") rfc_example ;;</x-ocaml></div><p>The pointer <code>/</code> has one token: the empty string. This accesses the member with an empty name.</p><h3 id="keys-with-special-characters"><a href="#keys-with-special-characters" class="anchor"></a>Keys with Special Characters</h3><p>The RFC example includes keys with <code>/</code> and <code>~</code> characters:</p><div class="x-ocaml-wrapper"><x-ocaml>get (of_string_nav "/a~1b") rfc_example ;;</x-ocaml></div><p>The token <code>a~1b</code> refers to the key <code>a/b</code>. We'll explain this escaping <a href="#escaping">below</a>.</p><div class="x-ocaml-wrapper"><x-ocaml>get (of_string_nav "/m~0n") rfc_example ;;</x-ocaml></div><p>The token <code>m~0n</code> refers to the key <code>m~n</code>.</p><p><b>Important</b>: When using the OCaml library programmatically, you don't need to worry about escaping. The <code>Mem</code> variant holds the literal key name:</p><div class="x-ocaml-wrapper"><x-ocaml>let slash_ptr = make [mem "a/b"];; 55 to_string slash_ptr;; 56 get slash_ptr rfc_example ;;</x-ocaml></div><p>The library escapes it when converting to string.</p><h3 id="other-special-characters-(no-escaping-needed)"><a href="#other-special-characters-(no-escaping-needed)" class="anchor"></a>Other Special Characters (No Escaping Needed)</h3><p>Most characters don't need escaping in JSON Pointer strings:</p><div class="x-ocaml-wrapper"><x-ocaml>get (of_string_nav "/c%d") rfc_example ;; ··· 62 val find : nav t -&gt; Jsont.json -&gt; Jsont.json option</pre><h3 id="array-index-rules"><a href="#array-index-rules" class="anchor"></a>Array Index Rules</h3><p><a href="https://datatracker.ietf.org/doc/html/rfc6901">RFC 6901</a> has specific rules for array indices. <a href="https://datatracker.ietf.org/doc/html/rfc6901#section-4">Section 4</a> states:</p><p><i>characters comprised of digits <code>...</code> that represent an unsigned base-10 integer value, making the new referenced value the array element with the zero-based index identified by the token</i></p><p>And importantly:</p><p><i>note that leading zeros are not allowed</i></p><div class="x-ocaml-wrapper"><x-ocaml>of_string_nav "/foo/0";;</x-ocaml></div><p>Zero itself is fine.</p><div class="x-ocaml-wrapper"><x-ocaml>of_string_nav "/foo/01";;</x-ocaml></div><p>But <code>01</code> has a leading zero, so it's NOT treated as an array index - it becomes a member name instead. This protects against accidental octal interpretation.</p><h2 id="the-end-of-array-marker:---and-type-safety"><a href="#the-end-of-array-marker:---and-type-safety" class="anchor"></a>The End-of-Array Marker: <code>-</code> and Type Safety</h2><p><a href="https://datatracker.ietf.org/doc/html/rfc6901#section-4">RFC 6901, Section 4</a> introduces a special token:</p><p><i>exactly the single character "-", making the new referenced value the (nonexistent) member after the last array element.</i></p><p>This <code>-</code> marker is unique to JSON Pointer (JSON Path has no equivalent). It's primarily useful for JSON Patch operations (<a href="https://datatracker.ietf.org/doc/html/rfc6902">RFC 6902</a>) to append elements to arrays.</p><h3 id="navigation-vs-append-pointers"><a href="#navigation-vs-append-pointers" class="anchor"></a>Navigation vs Append Pointers</h3><p>The <code>json-pointer</code> library uses <b>phantom types</b> to encode the difference between pointers that can be used for navigation and pointers that target the "append position":</p><pre>type nav (* A pointer to an existing element *) 63 type append (* A pointer ending with "-" (append position) *) 64 type 'a t (* Pointer with phantom type parameter *) 65 - type any (* Existential: wraps either nav or append *)</pre><p>When you parse a pointer with <code>of_string</code>, you get an <code>any</code> pointer that can be used directly with mutation operations:</p><div class="x-ocaml-wrapper"><x-ocaml>of_string "/foo/0";; 66 - of_string "/foo/-";;</x-ocaml></div><p>The <code>-</code> creates an append pointer. The <code>any</code> type wraps either kind, making it ergonomic to use with operations like <code>set</code> and <code>add</code>.</p><h3 id="why-two-pointer-types?"><a href="#why-two-pointer-types?" class="anchor"></a>Why Two Pointer Types?</h3><p>The RFC explains that <code>-</code> refers to a <em>nonexistent</em> position:</p><p><i>Note that the use of the "-" character to index an array will always result in such an error condition because by definition it refers to a nonexistent array element.</i></p><p>So you <b>cannot use <code>get</code> or <code>find</code></b> with an append pointer - it makes no sense to retrieve a value from a position that doesn't exist! The library enforces this:</p><ul><li>Use <code>of_string_nav</code> when you need to call <code>get</code> or <code>find</code></li><li>Use <code>of_string</code> (returns <code>any</code>) for mutation operations</li></ul><p>Mutation operations like <code>add</code> accept <code>any</code> directly:</p><div class="x-ocaml-wrapper"><x-ocaml>let arr_obj = parse_json {|{"foo":["a","b"]}|};; 67 - add (of_string "/foo/-") arr_obj ~value:(Jsont.Json.string "c");;</x-ocaml></div><p>For retrieval operations, use <code>of_string_nav</code> which ensures the pointer doesn't contain <code>-</code>:</p><div class="x-ocaml-wrapper"><x-ocaml>of_string_nav "/foo/0";; 68 - of_string_nav "/foo/-";;</x-ocaml></div><h3 id="creating-append-pointers-programmatically"><a href="#creating-append-pointers-programmatically" class="anchor"></a>Creating Append Pointers Programmatically</h3><p>You can convert a navigation pointer to an append pointer using <code>at_end</code>:</p><div class="x-ocaml-wrapper"><x-ocaml>let nav_ptr = of_string_nav "/foo";; 69 let app_ptr = at_end nav_ptr;; 70 - to_string app_ptr;;</x-ocaml></div><h2 id="mutation-operations"><a href="#mutation-operations" class="anchor"></a>Mutation Operations</h2><p>While <a href="https://datatracker.ietf.org/doc/html/rfc6901">RFC 6901</a> defines JSON Pointer for read-only access, <a href="https://datatracker.ietf.org/doc/html/rfc6902">RFC 6902</a> (JSON Patch) uses JSON Pointer for modifications. The <code>json-pointer</code> library provides these operations.</p><h3 id="add"><a href="#add" class="anchor"></a>Add</h3><p>The <code>add</code> operation inserts a value at a location. It accepts <code>any</code> pointers, so you can use <code>of_string</code> directly:</p><div class="x-ocaml-wrapper"><x-ocaml>let obj = parse_json {|{"foo":"bar"}|};; 71 - add (of_string "/baz") obj ~value:(Jsont.Json.string "qux");;</x-ocaml></div><p>For arrays, <code>add</code> inserts BEFORE the specified index:</p><div class="x-ocaml-wrapper"><x-ocaml>let arr_obj = parse_json {|{"foo":["a","b"]}|};; 72 - add (of_string "/foo/1") arr_obj ~value:(Jsont.Json.string "X");;</x-ocaml></div><p>This is where the <code>-</code> marker shines - it appends to the end:</p><div class="x-ocaml-wrapper"><x-ocaml>add (of_string "/foo/-") arr_obj ~value:(Jsont.Json.string "c");;</x-ocaml></div><p>You can also use <code>at_end</code> to create an append pointer programmatically:</p><div class="x-ocaml-wrapper"><x-ocaml>add (any (at_end (of_string_nav "/foo"))) arr_obj ~value:(Jsont.Json.string "c");;</x-ocaml></div><h3 id="ergonomic-mutation-with-any"><a href="#ergonomic-mutation-with-any" class="anchor"></a>Ergonomic Mutation with <code>any</code></h3><p>Since <code>add</code>, <code>set</code>, <code>move</code>, and <code>copy</code> accept <code>any</code> pointers, you can use <code>of_string</code> directly without any pattern matching. This makes JSON Patch implementations straightforward:</p><div class="x-ocaml-wrapper"><x-ocaml>let items = parse_json {|{"items":["x"]}|};; 73 add (of_string "/items/0") items ~value:(Jsont.Json.string "y");; 74 - add (of_string "/items/-") items ~value:(Jsont.Json.string "z");;</x-ocaml></div><p>The same pointer works whether it targets an existing position or the append marker - no conditional logic needed.</p><h3 id="remove"><a href="#remove" class="anchor"></a>Remove</h3><p>The <code>remove</code> operation deletes a value. It only accepts <code>nav t</code> because you can only remove something that exists:</p><div class="x-ocaml-wrapper"><x-ocaml>let two_fields = parse_json {|{"foo":"bar","baz":"qux"}|};; 75 remove (of_string_nav "/baz") two_fields ;;</x-ocaml></div><p>For arrays, it removes and shifts:</p><div class="x-ocaml-wrapper"><x-ocaml>let three_elem = parse_json {|{"foo":["a","b","c"]}|};; 76 - remove (of_string_nav "/foo/1") three_elem ;;</x-ocaml></div><h3 id="replace"><a href="#replace" class="anchor"></a>Replace</h3><p>The <code>replace</code> operation updates an existing value:</p><div class="x-ocaml-wrapper"><x-ocaml>replace (of_string_nav "/foo") obj ~value:(Jsont.Json.string "baz") 77 - ;;</x-ocaml></div><p>Unlike <code>add</code>, <code>replace</code> requires the target to already exist (hence <code>nav t</code>). Attempting to replace a nonexistent path raises an error.</p><h3 id="move"><a href="#move" class="anchor"></a>Move</h3><p>The <code>move</code> operation relocates a value. The source (<code>from</code>) must be a <code>nav t</code> (you can only move something that exists), but the destination (<code>path</code>) accepts <code>any</code>:</p><div class="x-ocaml-wrapper"><x-ocaml>let nested = parse_json {|{"foo":{"bar":"baz"},"qux":{}}|};; 78 - move ~from:(of_string_nav "/foo/bar") ~path:(of_string "/qux/thud") nested;;</x-ocaml></div><h3 id="copy"><a href="#copy" class="anchor"></a>Copy</h3><p>The <code>copy</code> operation duplicates a value (same typing as <code>move</code>):</p><div class="x-ocaml-wrapper"><x-ocaml>let to_copy = parse_json {|{"foo":{"bar":"baz"}}|};; 79 - copy ~from:(of_string_nav "/foo/bar") ~path:(of_string "/foo/qux") to_copy;;</x-ocaml></div><h3 id="test"><a href="#test" class="anchor"></a>Test</h3><p>The <code>test</code> operation verifies a value (useful in JSON Patch):</p><div class="x-ocaml-wrapper"><x-ocaml>test (of_string_nav "/foo") obj ~expected:(Jsont.Json.string "bar");; 80 test (of_string_nav "/foo") obj ~expected:(Jsont.Json.string "wrong");;</x-ocaml></div><h2 id="escaping"><a href="#escaping" class="anchor"></a>Escaping Special Characters</h2><p><a href="https://datatracker.ietf.org/doc/html/rfc6901#section-3">RFC 6901, Section 3</a> explains the escaping rules:</p><p><i>Because the characters '~' (%x7E) and '/' (%x2F) have special meanings in JSON Pointer, '~' needs to be encoded as '~0' and '/' needs to be encoded as '~1' when these characters appear in a reference token.</i></p><p>Why these specific characters?</p><ul><li><code>/</code> separates tokens, so it must be escaped inside a token</li><li><code>~</code> is the escape character itself, so it must also be escaped</li></ul><p>The escape sequences are:</p><ul><li><code>~0</code> represents <code>~</code> (tilde)</li><li><code>~1</code> represents <code>/</code> (forward slash)</li></ul><h3 id="the-library-handles-escaping-automatically"><a href="#the-library-handles-escaping-automatically" class="anchor"></a>The Library Handles Escaping Automatically</h3><p><b>Important</b>: When using <code>json-pointer</code> programmatically, you rarely need to think about escaping. The <code>Mem</code> variant stores unescaped strings, and escaping happens automatically during serialization:</p><div class="x-ocaml-wrapper"><x-ocaml>let p = make [mem "a/b"];; 81 to_string p;; 82 - of_string_nav "/a~1b";;</x-ocaml></div><h3 id="escaping-in-action"><a href="#escaping-in-action" class="anchor"></a>Escaping in Action</h3><p>The <code>Token</code> module exposes the escaping functions:</p><div class="x-ocaml-wrapper"><x-ocaml>Token.escape "hello";; 83 Token.escape "a/b";; 84 Token.escape "a~b";; 85 Token.escape "~/";;</x-ocaml></div><h3 id="unescaping"><a href="#unescaping" class="anchor"></a>Unescaping</h3><p>And the reverse process:</p><div class="x-ocaml-wrapper"><x-ocaml>Token.unescape "a~1b";; ··· 87 to_uri_fragment (of_string_nav "/a~1b");; 88 to_uri_fragment (of_string_nav "/c%d");; 89 to_uri_fragment (of_string_nav "/ ");;</x-ocaml></div><p>The <code>%</code> character must be percent-encoded as <code>%25</code> in URIs, and spaces become <code>%20</code>.</p><p>Here's the RFC example showing the URI fragment forms:</p><ul><li><code>""</code> -&gt; <code>#</code> -&gt; whole document</li><li><code>"/foo"</code> -&gt; <code>#/foo</code> -&gt; <code>["bar", "baz"]</code></li><li><code>"/foo/0"</code> -&gt; <code>#/foo/0</code> -&gt; <code>"bar"</code></li><li><code>"/"</code> -&gt; <code>#/</code> -&gt; <code>0</code></li><li><code>"/a~1b"</code> -&gt; <code>#/a~1b</code> -&gt; <code>1</code></li><li><code>"/c%d"</code> -&gt; <code>#/c%25d</code> -&gt; <code>2</code></li><li><code>"/ "</code> -&gt; <code>#/%20</code> -&gt; <code>7</code></li><li><code>"/m~0n"</code> -&gt; <code>#/m~0n</code> -&gt; <code>8</code></li></ul><h2 id="building-pointers-programmatically"><a href="#building-pointers-programmatically" class="anchor"></a>Building Pointers Programmatically</h2><p>Instead of parsing strings, you can build pointers from indices:</p><div class="x-ocaml-wrapper"><x-ocaml>let port_ptr = make [mem "database"; mem "port"];; 90 - to_string port_ptr;;</x-ocaml></div><p>For array access, use the <code>nth</code> helper:</p><div class="x-ocaml-wrapper"><x-ocaml>let first_feature_ptr = make [mem "features"; nth 0];; 91 - to_string first_feature_ptr;;</x-ocaml></div><h3 id="pointer-navigation"><a href="#pointer-navigation" class="anchor"></a>Pointer Navigation</h3><p>You can build pointers incrementally using the <code>/</code> operator (or <code>append_index</code>):</p><div class="x-ocaml-wrapper"><x-ocaml>let db_ptr = of_string_nav "/database";; 92 let creds_ptr = db_ptr / mem "credentials";; 93 let user_ptr = creds_ptr / mem "username";; 94 to_string user_ptr;;</x-ocaml></div><p>Or concatenate two pointers:</p><div class="x-ocaml-wrapper"><x-ocaml>let base = of_string_nav "/api/v1";; ··· 100 "credentials": {"username": "admin", "password": "secret"} 101 }, 102 "features": ["auth", "logging", "metrics"] 103 - }|};;</x-ocaml></div><h3 id="typed-access-with-path"><a href="#typed-access-with-path" class="anchor"></a>Typed Access with <code>path</code></h3><p>The <code>path</code> combinator combines pointer navigation with typed decoding:</p><div class="x-ocaml-wrapper"><x-ocaml>let nav = of_string_nav "/database/host";; 104 let db_host = 105 Jsont.Json.decode 106 (path nav Jsont.string) ··· 138 Jsont.Json.decode 139 (path (of_string_nav "/database/port") Jsont.int) 140 config_json 141 - |&gt; Result.get_ok;;</x-ocaml></div><p>The typed approach catches mismatches at decode time with clear errors.</p><h3 id="updates-with-polymorphic-pointers"><a href="#updates-with-polymorphic-pointers" class="anchor"></a>Updates with Polymorphic Pointers</h3><p>The <code>set</code> and <code>add</code> functions accept <code>any</code> pointers, which means you can use the result of <code>of_string</code> directly without pattern matching:</p><div class="x-ocaml-wrapper"><x-ocaml>let tasks = parse_json {|{"tasks":["buy milk"]}|};; 142 set (of_string "/tasks/0") tasks ~value:(Jsont.Json.string "buy eggs");; 143 - set (of_string "/tasks/-") tasks ~value:(Jsont.Json.string "call mom");;</x-ocaml></div><p>This is useful for implementing JSON Patch (<a href="https://datatracker.ietf.org/doc/html/rfc6902">RFC 6902</a>) where operations like <code>"add"</code> can target either existing positions or the append marker. If you need to distinguish between pointer types at runtime, use <code>of_string_kind</code> which returns a polymorphic variant:</p><div class="x-ocaml-wrapper"><x-ocaml>of_string_kind "/tasks/0";; 144 of_string_kind "/tasks/-";;</x-ocaml></div><h2 id="summary"><a href="#summary" class="anchor"></a>Summary</h2><p>JSON Pointer (<a href="https://datatracker.ietf.org/doc/html/rfc6901">RFC 6901</a>) provides a simple but powerful way to address values within JSON documents:</p><ol><li><b>Syntax</b>: Pointers are strings of <code>/</code>-separated reference tokens</li><li><b>Escaping</b>: Use <code>~0</code> for <code>~</code> and <code>~1</code> for <code>/</code> in tokens (handled automatically by the library)</li><li><b>Evaluation</b>: Tokens navigate through objects (by key) and arrays (by index)</li><li><b>URI Encoding</b>: Pointers can be percent-encoded for use in URIs</li><li><b>Mutations</b>: Combined with JSON Patch (<a href="https://datatracker.ietf.org/doc/html/rfc6902">RFC 6902</a>), pointers enable structured updates</li><li><b>Type Safety</b>: 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></ol><p>The <code>json-pointer</code> library implements all of this with type-safe OCaml interfaces, integration with the <code>jsont</code> codec system, and proper error handling for malformed pointers and missing values.</p><h3 id="key-points-on-json-pointer-vs-json-path"><a href="#key-points-on-json-pointer-vs-json-path" class="anchor"></a>Key Points on JSON Pointer vs JSON Path</h3><ul><li><b>JSON Pointer</b> addresses a <em>single</em> location (like a file path)</li><li><b>JSON Path</b> queries for <em>multiple</em> values (like a search)</li><li>The <code>-</code> token is unique to JSON Pointer - it means "append position" for arrays</li><li>The library uses phantom types to enforce that <code>-</code> (append) pointers cannot be used with <code>get</code>/<code>find</code></li></ul></div> 145 </body></html>
··· 50 "k\"l": 6, 51 " ": 7, 52 "m~n": 8 53 + }|};;</x-ocaml></div><p>This document is carefully constructed to exercise various edge cases!</p><h3 id="the-root-pointer"><a href="#the-root-pointer" class="anchor"></a>The Root Pointer</h3><div class="x-ocaml-wrapper"><x-ocaml>get root rfc_example ;;</x-ocaml></div><p>The empty pointer (<code>Json_pointer.root</code>) returns the whole document.</p><h3 id="object-member-access"><a href="#object-member-access" class="anchor"></a>Object Member Access</h3><div class="x-ocaml-wrapper"><x-ocaml>get (of_string_nav "/foo") rfc_example ;;</x-ocaml></div><p><code>/foo</code> accesses the member named <code>foo</code>, which is an array.</p><h3 id="array-index-access"><a href="#array-index-access" class="anchor"></a>Array Index Access</h3><div class="x-ocaml-wrapper"><x-ocaml>get (of_string_nav "/foo/0") rfc_example ;; 54 get (of_string_nav "/foo/1") rfc_example ;;</x-ocaml></div><p><code>/foo/0</code> first goes to <code>foo</code>, then accesses index 0 of the array.</p><h3 id="empty-string-as-key"><a href="#empty-string-as-key" class="anchor"></a>Empty String as Key</h3><p>JSON allows empty strings as object keys:</p><div class="x-ocaml-wrapper"><x-ocaml>get (of_string_nav "/") rfc_example ;;</x-ocaml></div><p>The pointer <code>/</code> has one token: the empty string. This accesses the member with an empty name.</p><h3 id="keys-with-special-characters"><a href="#keys-with-special-characters" class="anchor"></a>Keys with Special Characters</h3><p>The RFC example includes keys with <code>/</code> and <code>~</code> characters:</p><div class="x-ocaml-wrapper"><x-ocaml>get (of_string_nav "/a~1b") rfc_example ;;</x-ocaml></div><p>The token <code>a~1b</code> refers to the key <code>a/b</code>. We'll explain this escaping <a href="#escaping">below</a>.</p><div class="x-ocaml-wrapper"><x-ocaml>get (of_string_nav "/m~0n") rfc_example ;;</x-ocaml></div><p>The token <code>m~0n</code> refers to the key <code>m~n</code>.</p><p><b>Important</b>: When using the OCaml library programmatically, you don't need to worry about escaping. The <code>Mem</code> variant holds the literal key name:</p><div class="x-ocaml-wrapper"><x-ocaml>let slash_ptr = make [mem "a/b"];; 55 to_string slash_ptr;; 56 get slash_ptr rfc_example ;;</x-ocaml></div><p>The library escapes it when converting to string.</p><h3 id="other-special-characters-(no-escaping-needed)"><a href="#other-special-characters-(no-escaping-needed)" class="anchor"></a>Other Special Characters (No Escaping Needed)</h3><p>Most characters don't need escaping in JSON Pointer strings:</p><div class="x-ocaml-wrapper"><x-ocaml>get (of_string_nav "/c%d") rfc_example ;; ··· 62 val find : nav t -&gt; Jsont.json -&gt; Jsont.json option</pre><h3 id="array-index-rules"><a href="#array-index-rules" class="anchor"></a>Array Index Rules</h3><p><a href="https://datatracker.ietf.org/doc/html/rfc6901">RFC 6901</a> has specific rules for array indices. <a href="https://datatracker.ietf.org/doc/html/rfc6901#section-4">Section 4</a> states:</p><p><i>characters comprised of digits <code>...</code> that represent an unsigned base-10 integer value, making the new referenced value the array element with the zero-based index identified by the token</i></p><p>And importantly:</p><p><i>note that leading zeros are not allowed</i></p><div class="x-ocaml-wrapper"><x-ocaml>of_string_nav "/foo/0";;</x-ocaml></div><p>Zero itself is fine.</p><div class="x-ocaml-wrapper"><x-ocaml>of_string_nav "/foo/01";;</x-ocaml></div><p>But <code>01</code> has a leading zero, so it's NOT treated as an array index - it becomes a member name instead. This protects against accidental octal interpretation.</p><h2 id="the-end-of-array-marker:---and-type-safety"><a href="#the-end-of-array-marker:---and-type-safety" class="anchor"></a>The End-of-Array Marker: <code>-</code> and Type Safety</h2><p><a href="https://datatracker.ietf.org/doc/html/rfc6901#section-4">RFC 6901, Section 4</a> introduces a special token:</p><p><i>exactly the single character "-", making the new referenced value the (nonexistent) member after the last array element.</i></p><p>This <code>-</code> marker is unique to JSON Pointer (JSON Path has no equivalent). It's primarily useful for JSON Patch operations (<a href="https://datatracker.ietf.org/doc/html/rfc6902">RFC 6902</a>) to append elements to arrays.</p><h3 id="navigation-vs-append-pointers"><a href="#navigation-vs-append-pointers" class="anchor"></a>Navigation vs Append Pointers</h3><p>The <code>json-pointer</code> library uses <b>phantom types</b> to encode the difference between pointers that can be used for navigation and pointers that target the "append position":</p><pre>type nav (* A pointer to an existing element *) 63 type append (* A pointer ending with "-" (append position) *) 64 type 'a t (* Pointer with phantom type parameter *) 65 + type any (* Existential: wraps either nav or append *)</pre><p>When you parse a pointer with <code>Json_pointer.of_string</code>, you get an <code>Json_pointer.any</code> pointer that can be used directly with mutation operations:</p><div class="x-ocaml-wrapper"><x-ocaml>of_string "/foo/0";; 66 + of_string "/foo/-";;</x-ocaml></div><p>The <code>-</code> creates an append pointer. The <code>Json_pointer.any</code> type wraps either kind, making it ergonomic to use with operations like <code>Json_pointer.set</code> and <code>Json_pointer.add</code>.</p><h3 id="why-two-pointer-types?"><a href="#why-two-pointer-types?" class="anchor"></a>Why Two Pointer Types?</h3><p>The RFC explains that <code>-</code> refers to a <em>nonexistent</em> position:</p><p><i>Note that the use of the "-" character to index an array will always result in such an error condition because by definition it refers to a nonexistent array element.</i></p><p>So you <b>cannot use <code>get</code> or <code>find</code></b> with an append pointer - it makes no sense to retrieve a value from a position that doesn't exist! The library enforces this:</p><ul><li>Use <code>Json_pointer.of_string_nav</code> when you need to call <code>Json_pointer.get</code> or <code>Json_pointer.find</code></li><li>Use <code>Json_pointer.of_string</code> (returns <code>Json_pointer.any</code>) for mutation operations</li></ul><p>Mutation operations like <code>Json_pointer.add</code> accept <code>Json_pointer.any</code> directly:</p><div class="x-ocaml-wrapper"><x-ocaml>let arr_obj = parse_json {|{"foo":["a","b"]}|};; 67 + add (of_string "/foo/-") arr_obj ~value:(Jsont.Json.string "c");;</x-ocaml></div><p>For retrieval operations, use <code>Json_pointer.of_string_nav</code> which ensures the pointer doesn't contain <code>-</code>:</p><div class="x-ocaml-wrapper"><x-ocaml>of_string_nav "/foo/0";; 68 + of_string_nav "/foo/-";;</x-ocaml></div><h3 id="creating-append-pointers-programmatically"><a href="#creating-append-pointers-programmatically" class="anchor"></a>Creating Append Pointers Programmatically</h3><p>You can convert a navigation pointer to an append pointer using <code>Json_pointer.at_end</code>:</p><div class="x-ocaml-wrapper"><x-ocaml>let nav_ptr = of_string_nav "/foo";; 69 let app_ptr = at_end nav_ptr;; 70 + to_string app_ptr;;</x-ocaml></div><h2 id="mutation-operations"><a href="#mutation-operations" class="anchor"></a>Mutation Operations</h2><p>While <a href="https://datatracker.ietf.org/doc/html/rfc6901">RFC 6901</a> defines JSON Pointer for read-only access, <a href="https://datatracker.ietf.org/doc/html/rfc6902">RFC 6902</a> (JSON Patch) uses JSON Pointer for modifications. The <code>json-pointer</code> library provides these operations.</p><h3 id="add"><a href="#add" class="anchor"></a>Add</h3><p>The <code>Json_pointer.add</code> operation inserts a value at a location. It accepts <code>Json_pointer.any</code> pointers, so you can use <code>Json_pointer.of_string</code> directly:</p><div class="x-ocaml-wrapper"><x-ocaml>let obj = parse_json {|{"foo":"bar"}|};; 71 + add (of_string "/baz") obj ~value:(Jsont.Json.string "qux");;</x-ocaml></div><p>For arrays, <code>Json_pointer.add</code> inserts BEFORE the specified index:</p><div class="x-ocaml-wrapper"><x-ocaml>let arr_obj = parse_json {|{"foo":["a","b"]}|};; 72 + add (of_string "/foo/1") arr_obj ~value:(Jsont.Json.string "X");;</x-ocaml></div><p>This is where the <code>-</code> marker shines - it appends to the end:</p><div class="x-ocaml-wrapper"><x-ocaml>add (of_string "/foo/-") arr_obj ~value:(Jsont.Json.string "c");;</x-ocaml></div><p>You can also use <code>Json_pointer.at_end</code> to create an append pointer programmatically:</p><div class="x-ocaml-wrapper"><x-ocaml>add (any (at_end (of_string_nav "/foo"))) arr_obj ~value:(Jsont.Json.string "c");;</x-ocaml></div><h3 id="ergonomic-mutation-with-any"><a href="#ergonomic-mutation-with-any" class="anchor"></a>Ergonomic Mutation with <code>any</code></h3><p>Since <code>Json_pointer.add</code>, <code>Json_pointer.set</code>, <code>Json_pointer.move</code>, and <code>Json_pointer.copy</code> accept <code>Json_pointer.any</code> pointers, you can use <code>Json_pointer.of_string</code> directly without any pattern matching. This makes JSON Patch implementations straightforward:</p><div class="x-ocaml-wrapper"><x-ocaml>let items = parse_json {|{"items":["x"]}|};; 73 add (of_string "/items/0") items ~value:(Jsont.Json.string "y");; 74 + add (of_string "/items/-") items ~value:(Jsont.Json.string "z");;</x-ocaml></div><p>The same pointer works whether it targets an existing position or the append marker - no conditional logic needed.</p><h3 id="remove"><a href="#remove" class="anchor"></a>Remove</h3><p>The <code>Json_pointer.remove</code> operation deletes a value. It only accepts <code>nav t</code> because you can only remove something that exists:</p><div class="x-ocaml-wrapper"><x-ocaml>let two_fields = parse_json {|{"foo":"bar","baz":"qux"}|};; 75 remove (of_string_nav "/baz") two_fields ;;</x-ocaml></div><p>For arrays, it removes and shifts:</p><div class="x-ocaml-wrapper"><x-ocaml>let three_elem = parse_json {|{"foo":["a","b","c"]}|};; 76 + remove (of_string_nav "/foo/1") three_elem ;;</x-ocaml></div><h3 id="replace"><a href="#replace" class="anchor"></a>Replace</h3><p>The <code>Json_pointer.replace</code> operation updates an existing value:</p><div class="x-ocaml-wrapper"><x-ocaml>replace (of_string_nav "/foo") obj ~value:(Jsont.Json.string "baz") 77 + ;;</x-ocaml></div><p>Unlike <code>Json_pointer.add</code>, <code>Json_pointer.replace</code> requires the target to already exist (hence <code>nav t</code>). Attempting to replace a nonexistent path raises an error.</p><h3 id="move"><a href="#move" class="anchor"></a>Move</h3><p>The <code>Json_pointer.move</code> operation relocates a value. The source (<code>from</code>) must be a <code>nav t</code> (you can only move something that exists), but the destination (<code>path</code>) accepts <code>Json_pointer.any</code>:</p><div class="x-ocaml-wrapper"><x-ocaml>let nested = parse_json {|{"foo":{"bar":"baz"},"qux":{}}|};; 78 + move ~from:(of_string_nav "/foo/bar") ~path:(of_string "/qux/thud") nested;;</x-ocaml></div><h3 id="copy"><a href="#copy" class="anchor"></a>Copy</h3><p>The <code>Json_pointer.copy</code> operation duplicates a value (same typing as <code>Json_pointer.move</code>):</p><div class="x-ocaml-wrapper"><x-ocaml>let to_copy = parse_json {|{"foo":{"bar":"baz"}}|};; 79 + copy ~from:(of_string_nav "/foo/bar") ~path:(of_string "/foo/qux") to_copy;;</x-ocaml></div><h3 id="test"><a href="#test" class="anchor"></a>Test</h3><p>The <code>Json_pointer.test</code> operation verifies a value (useful in JSON Patch):</p><div class="x-ocaml-wrapper"><x-ocaml>test (of_string_nav "/foo") obj ~expected:(Jsont.Json.string "bar");; 80 test (of_string_nav "/foo") obj ~expected:(Jsont.Json.string "wrong");;</x-ocaml></div><h2 id="escaping"><a href="#escaping" class="anchor"></a>Escaping Special Characters</h2><p><a href="https://datatracker.ietf.org/doc/html/rfc6901#section-3">RFC 6901, Section 3</a> explains the escaping rules:</p><p><i>Because the characters '~' (%x7E) and '/' (%x2F) have special meanings in JSON Pointer, '~' needs to be encoded as '~0' and '/' needs to be encoded as '~1' when these characters appear in a reference token.</i></p><p>Why these specific characters?</p><ul><li><code>/</code> separates tokens, so it must be escaped inside a token</li><li><code>~</code> is the escape character itself, so it must also be escaped</li></ul><p>The escape sequences are:</p><ul><li><code>~0</code> represents <code>~</code> (tilde)</li><li><code>~1</code> represents <code>/</code> (forward slash)</li></ul><h3 id="the-library-handles-escaping-automatically"><a href="#the-library-handles-escaping-automatically" class="anchor"></a>The Library Handles Escaping Automatically</h3><p><b>Important</b>: When using <code>json-pointer</code> programmatically, you rarely need to think about escaping. The <code>Mem</code> variant stores unescaped strings, and escaping happens automatically during serialization:</p><div class="x-ocaml-wrapper"><x-ocaml>let p = make [mem "a/b"];; 81 to_string p;; 82 + of_string_nav "/a~1b";;</x-ocaml></div><h3 id="escaping-in-action"><a href="#escaping-in-action" class="anchor"></a>Escaping in Action</h3><p>The <code>Json_pointer.Token</code> module exposes the escaping functions:</p><div class="x-ocaml-wrapper"><x-ocaml>Token.escape "hello";; 83 Token.escape "a/b";; 84 Token.escape "a~b";; 85 Token.escape "~/";;</x-ocaml></div><h3 id="unescaping"><a href="#unescaping" class="anchor"></a>Unescaping</h3><p>And the reverse process:</p><div class="x-ocaml-wrapper"><x-ocaml>Token.unescape "a~1b";; ··· 87 to_uri_fragment (of_string_nav "/a~1b");; 88 to_uri_fragment (of_string_nav "/c%d");; 89 to_uri_fragment (of_string_nav "/ ");;</x-ocaml></div><p>The <code>%</code> character must be percent-encoded as <code>%25</code> in URIs, and spaces become <code>%20</code>.</p><p>Here's the RFC example showing the URI fragment forms:</p><ul><li><code>""</code> -&gt; <code>#</code> -&gt; whole document</li><li><code>"/foo"</code> -&gt; <code>#/foo</code> -&gt; <code>["bar", "baz"]</code></li><li><code>"/foo/0"</code> -&gt; <code>#/foo/0</code> -&gt; <code>"bar"</code></li><li><code>"/"</code> -&gt; <code>#/</code> -&gt; <code>0</code></li><li><code>"/a~1b"</code> -&gt; <code>#/a~1b</code> -&gt; <code>1</code></li><li><code>"/c%d"</code> -&gt; <code>#/c%25d</code> -&gt; <code>2</code></li><li><code>"/ "</code> -&gt; <code>#/%20</code> -&gt; <code>7</code></li><li><code>"/m~0n"</code> -&gt; <code>#/m~0n</code> -&gt; <code>8</code></li></ul><h2 id="building-pointers-programmatically"><a href="#building-pointers-programmatically" class="anchor"></a>Building Pointers Programmatically</h2><p>Instead of parsing strings, you can build pointers from indices:</p><div class="x-ocaml-wrapper"><x-ocaml>let port_ptr = make [mem "database"; mem "port"];; 90 + to_string port_ptr;;</x-ocaml></div><p>For array access, use the <code>Json_pointer.nth</code> helper:</p><div class="x-ocaml-wrapper"><x-ocaml>let first_feature_ptr = make [mem "features"; nth 0];; 91 + to_string first_feature_ptr;;</x-ocaml></div><h3 id="pointer-navigation"><a href="#pointer-navigation" class="anchor"></a>Pointer Navigation</h3><p>You can build pointers incrementally using the <code>/</code> operator (or <code>Json_pointer.append_index</code>):</p><div class="x-ocaml-wrapper"><x-ocaml>let db_ptr = of_string_nav "/database";; 92 let creds_ptr = db_ptr / mem "credentials";; 93 let user_ptr = creds_ptr / mem "username";; 94 to_string user_ptr;;</x-ocaml></div><p>Or concatenate two pointers:</p><div class="x-ocaml-wrapper"><x-ocaml>let base = of_string_nav "/api/v1";; ··· 100 "credentials": {"username": "admin", "password": "secret"} 101 }, 102 "features": ["auth", "logging", "metrics"] 103 + }|};;</x-ocaml></div><h3 id="typed-access-with-path"><a href="#typed-access-with-path" class="anchor"></a>Typed Access with <code>path</code></h3><p>The <code>Json_pointer.path</code> combinator combines pointer navigation with typed decoding:</p><div class="x-ocaml-wrapper"><x-ocaml>let nav = of_string_nav "/database/host";; 104 let db_host = 105 Jsont.Json.decode 106 (path nav Jsont.string) ··· 138 Jsont.Json.decode 139 (path (of_string_nav "/database/port") Jsont.int) 140 config_json 141 + |&gt; Result.get_ok;;</x-ocaml></div><p>The typed approach catches mismatches at decode time with clear errors.</p><h3 id="updates-with-polymorphic-pointers"><a href="#updates-with-polymorphic-pointers" class="anchor"></a>Updates with Polymorphic Pointers</h3><p>The <code>Json_pointer.set</code> and <code>Json_pointer.add</code> functions accept <code>Json_pointer.any</code> pointers, which means you can use the result of <code>Json_pointer.of_string</code> directly without pattern matching:</p><div class="x-ocaml-wrapper"><x-ocaml>let tasks = parse_json {|{"tasks":["buy milk"]}|};; 142 set (of_string "/tasks/0") tasks ~value:(Jsont.Json.string "buy eggs");; 143 + set (of_string "/tasks/-") tasks ~value:(Jsont.Json.string "call mom");;</x-ocaml></div><p>This is useful for implementing JSON Patch (<a href="https://datatracker.ietf.org/doc/html/rfc6902">RFC 6902</a>) where operations like <code>"add"</code> can target either existing positions or the append marker. If you need to distinguish between pointer types at runtime, use <code>Json_pointer.of_string_kind</code> which returns a polymorphic variant:</p><div class="x-ocaml-wrapper"><x-ocaml>of_string_kind "/tasks/0";; 144 of_string_kind "/tasks/-";;</x-ocaml></div><h2 id="summary"><a href="#summary" class="anchor"></a>Summary</h2><p>JSON Pointer (<a href="https://datatracker.ietf.org/doc/html/rfc6901">RFC 6901</a>) provides a simple but powerful way to address values within JSON documents:</p><ol><li><b>Syntax</b>: Pointers are strings of <code>/</code>-separated reference tokens</li><li><b>Escaping</b>: Use <code>~0</code> for <code>~</code> and <code>~1</code> for <code>/</code> in tokens (handled automatically by the library)</li><li><b>Evaluation</b>: Tokens navigate through objects (by key) and arrays (by index)</li><li><b>URI Encoding</b>: Pointers can be percent-encoded for use in URIs</li><li><b>Mutations</b>: Combined with JSON Patch (<a href="https://datatracker.ietf.org/doc/html/rfc6902">RFC 6902</a>), pointers enable structured updates</li><li><b>Type Safety</b>: 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></ol><p>The <code>json-pointer</code> library implements all of this with type-safe OCaml interfaces, integration with the <code>jsont</code> codec system, and proper error handling for malformed pointers and missing values.</p><h3 id="key-points-on-json-pointer-vs-json-path"><a href="#key-points-on-json-pointer-vs-json-path" class="anchor"></a>Key Points on JSON Pointer vs JSON Path</h3><ul><li><b>JSON Pointer</b> addresses a <em>single</em> location (like a file path)</li><li><b>JSON Path</b> queries for <em>multiple</em> values (like a search)</li><li>The <code>-</code> token is unique to JSON Pointer - it means "append position" for arrays</li><li>The library uses phantom types to enforce that <code>-</code> (append) pointers cannot be used with <code>get</code>/<code>find</code></li></ul></div> 145 </body></html>
+20 -20
doc/tutorial.html
··· 44 val rfc_example : Jsont.json = 45 {&quot;foo&quot;:[&quot;bar&quot;,&quot;baz&quot;],&quot;&quot;:0,&quot;a/b&quot;:1,&quot;c%d&quot;:2,&quot;e^f&quot;:3,&quot;g|h&quot;:4,&quot;i\\j&quot;:5,&quot;k\&quot;l&quot;:6,&quot; &quot;:7,&quot;m~n&quot;:8}</code></pre><p>This document is carefully constructed to exercise various edge cases!</p><h3 id="the-root-pointer"><a href="#the-root-pointer" class="anchor"></a>The Root Pointer</h3><pre class="language-ocaml"><code># get root rfc_example ;; 46 - : Jsont.json = 47 - {&quot;foo&quot;:[&quot;bar&quot;,&quot;baz&quot;],&quot;&quot;:0,&quot;a/b&quot;:1,&quot;c%d&quot;:2,&quot;e^f&quot;:3,&quot;g|h&quot;:4,&quot;i\\j&quot;:5,&quot;k\&quot;l&quot;:6,&quot; &quot;:7,&quot;m~n&quot;:8}</code></pre><p>The empty pointer (<code>root</code>) returns the whole document.</p><h3 id="object-member-access"><a href="#object-member-access" class="anchor"></a>Object Member Access</h3><pre class="language-ocaml"><code># get (of_string_nav &quot;/foo&quot;) rfc_example ;; 48 - : Jsont.json = [&quot;bar&quot;,&quot;baz&quot;]</code></pre><p><code>/foo</code> accesses the member named <code>foo</code>, which is an array.</p><h3 id="array-index-access"><a href="#array-index-access" class="anchor"></a>Array Index Access</h3><pre class="language-ocaml"><code># get (of_string_nav &quot;/foo/0&quot;) rfc_example ;; 49 - : Jsont.json = &quot;bar&quot; 50 # get (of_string_nav &quot;/foo/1&quot;) rfc_example ;; ··· 77 - : nav t = [Mem &quot;foo&quot;; Mem &quot;01&quot;]</code></pre><p>But <code>01</code> has a leading zero, so it's NOT treated as an array index - it becomes a member name instead. This protects against accidental octal interpretation.</p><h2 id="the-end-of-array-marker:---and-type-safety"><a href="#the-end-of-array-marker:---and-type-safety" class="anchor"></a>The End-of-Array Marker: <code>-</code> and Type Safety</h2><p><a href="https://datatracker.ietf.org/doc/html/rfc6901#section-4">RFC 6901, Section 4</a> introduces a special token:</p><p><i>exactly the single character &quot;-&quot;, making the new referenced value the (nonexistent) member after the last array element.</i></p><p>This <code>-</code> marker is unique to JSON Pointer (JSON Path has no equivalent). It's primarily useful for JSON Patch operations (<a href="https://datatracker.ietf.org/doc/html/rfc6902">RFC 6902</a>) to append elements to arrays.</p><h3 id="navigation-vs-append-pointers"><a href="#navigation-vs-append-pointers" class="anchor"></a>Navigation vs Append Pointers</h3><p>The <code>json-pointer</code> library uses <b>phantom types</b> to encode the difference between pointers that can be used for navigation and pointers that target the &quot;append position&quot;:</p><pre>type nav (* A pointer to an existing element *) 78 type append (* A pointer ending with &quot;-&quot; (append position) *) 79 type 'a t (* Pointer with phantom type parameter *) 80 - type any (* Existential: wraps either nav or append *)</pre><p>When you parse a pointer with <code>of_string</code>, you get an <code>any</code> pointer that can be used directly with mutation operations:</p><pre class="language-ocaml"><code># of_string &quot;/foo/0&quot;;; 81 - : any = Any &lt;abstr&gt; 82 # of_string &quot;/foo/-&quot;;; 83 - - : any = Any &lt;abstr&gt;</code></pre><p>The <code>-</code> creates an append pointer. The <code>any</code> type wraps either kind, making it ergonomic to use with operations like <code>set</code> and <code>add</code>.</p><h3 id="why-two-pointer-types?"><a href="#why-two-pointer-types?" class="anchor"></a>Why Two Pointer Types?</h3><p>The RFC explains that <code>-</code> refers to a <em>nonexistent</em> position:</p><p><i>Note that the use of the &quot;-&quot; character to index an array will always result in such an error condition because by definition it refers to a nonexistent array element.</i></p><p>So you <b>cannot use <code>get</code> or <code>find</code></b> with an append pointer - it makes no sense to retrieve a value from a position that doesn't exist! The library enforces this:</p><ul><li>Use <code>of_string_nav</code> when you need to call <code>get</code> or <code>find</code></li><li>Use <code>of_string</code> (returns <code>any</code>) for mutation operations</li></ul><p>Mutation operations like <code>add</code> accept <code>any</code> directly:</p><pre class="language-ocaml"><code># let arr_obj = parse_json {|{&quot;foo&quot;:[&quot;a&quot;,&quot;b&quot;]}|};; 84 val arr_obj : Jsont.json = {&quot;foo&quot;:[&quot;a&quot;,&quot;b&quot;]} 85 # add (of_string &quot;/foo/-&quot;) arr_obj ~value:(Jsont.Json.string &quot;c&quot;);; 86 - - : Jsont.json = {&quot;foo&quot;:[&quot;a&quot;,&quot;b&quot;,&quot;c&quot;]}</code></pre><p>For retrieval operations, use <code>of_string_nav</code> which ensures the pointer doesn't contain <code>-</code>:</p><pre class="language-ocaml"><code># of_string_nav &quot;/foo/0&quot;;; 87 - : nav t = [Mem &quot;foo&quot;; Nth 0] 88 # of_string_nav &quot;/foo/-&quot;;; 89 Exception: 90 - Jsont.Error Invalid JSON Pointer: '-' not allowed in navigation pointer.</code></pre><h3 id="creating-append-pointers-programmatically"><a href="#creating-append-pointers-programmatically" class="anchor"></a>Creating Append Pointers Programmatically</h3><p>You can convert a navigation pointer to an append pointer using <code>at_end</code>:</p><pre class="language-ocaml"><code># let nav_ptr = of_string_nav &quot;/foo&quot;;; 91 val nav_ptr : nav t = [Mem &quot;foo&quot;] 92 # let app_ptr = at_end nav_ptr;; 93 val app_ptr : append t = [Mem &quot;foo&quot;] /- 94 # to_string app_ptr;; 95 - - : string = &quot;/foo/-&quot;</code></pre><h2 id="mutation-operations"><a href="#mutation-operations" class="anchor"></a>Mutation Operations</h2><p>While <a href="https://datatracker.ietf.org/doc/html/rfc6901">RFC 6901</a> defines JSON Pointer for read-only access, <a href="https://datatracker.ietf.org/doc/html/rfc6902">RFC 6902</a> (JSON Patch) uses JSON Pointer for modifications. The <code>json-pointer</code> library provides these operations.</p><h3 id="add"><a href="#add" class="anchor"></a>Add</h3><p>The <code>add</code> operation inserts a value at a location. It accepts <code>any</code> pointers, so you can use <code>of_string</code> directly:</p><pre class="language-ocaml"><code># let obj = parse_json {|{&quot;foo&quot;:&quot;bar&quot;}|};; 96 val obj : Jsont.json = {&quot;foo&quot;:&quot;bar&quot;} 97 # add (of_string &quot;/baz&quot;) obj ~value:(Jsont.Json.string &quot;qux&quot;);; 98 - - : Jsont.json = {&quot;foo&quot;:&quot;bar&quot;,&quot;baz&quot;:&quot;qux&quot;}</code></pre><p>For arrays, <code>add</code> inserts BEFORE the specified index:</p><pre class="language-ocaml"><code># let arr_obj = parse_json {|{&quot;foo&quot;:[&quot;a&quot;,&quot;b&quot;]}|};; 99 val arr_obj : Jsont.json = {&quot;foo&quot;:[&quot;a&quot;,&quot;b&quot;]} 100 # add (of_string &quot;/foo/1&quot;) arr_obj ~value:(Jsont.Json.string &quot;X&quot;);; 101 - : Jsont.json = {&quot;foo&quot;:[&quot;a&quot;,&quot;X&quot;,&quot;b&quot;]}</code></pre><p>This is where the <code>-</code> marker shines - it appends to the end:</p><pre class="language-ocaml"><code># add (of_string &quot;/foo/-&quot;) arr_obj ~value:(Jsont.Json.string &quot;c&quot;);; 102 - - : Jsont.json = {&quot;foo&quot;:[&quot;a&quot;,&quot;b&quot;,&quot;c&quot;]}</code></pre><p>You can also use <code>at_end</code> to create an append pointer programmatically:</p><pre class="language-ocaml"><code># add (any (at_end (of_string_nav &quot;/foo&quot;))) arr_obj ~value:(Jsont.Json.string &quot;c&quot;);; 103 - - : Jsont.json = {&quot;foo&quot;:[&quot;a&quot;,&quot;b&quot;,&quot;c&quot;]}</code></pre><h3 id="ergonomic-mutation-with-any"><a href="#ergonomic-mutation-with-any" class="anchor"></a>Ergonomic Mutation with <code>any</code></h3><p>Since <code>add</code>, <code>set</code>, <code>move</code>, and <code>copy</code> accept <code>any</code> pointers, you can use <code>of_string</code> directly without any pattern matching. This makes JSON Patch implementations straightforward:</p><pre class="language-ocaml"><code># let items = parse_json {|{&quot;items&quot;:[&quot;x&quot;]}|};; 104 val items : Jsont.json = {&quot;items&quot;:[&quot;x&quot;]} 105 # add (of_string &quot;/items/0&quot;) items ~value:(Jsont.Json.string &quot;y&quot;);; 106 - : Jsont.json = {&quot;items&quot;:[&quot;y&quot;,&quot;x&quot;]} 107 # add (of_string &quot;/items/-&quot;) items ~value:(Jsont.Json.string &quot;z&quot;);; 108 - - : Jsont.json = {&quot;items&quot;:[&quot;x&quot;,&quot;z&quot;]}</code></pre><p>The same pointer works whether it targets an existing position or the append marker - no conditional logic needed.</p><h3 id="remove"><a href="#remove" class="anchor"></a>Remove</h3><p>The <code>remove</code> operation deletes a value. It only accepts <code>nav t</code> because you can only remove something that exists:</p><pre class="language-ocaml"><code># let two_fields = parse_json {|{&quot;foo&quot;:&quot;bar&quot;,&quot;baz&quot;:&quot;qux&quot;}|};; 109 val two_fields : Jsont.json = {&quot;foo&quot;:&quot;bar&quot;,&quot;baz&quot;:&quot;qux&quot;} 110 # remove (of_string_nav &quot;/baz&quot;) two_fields ;; 111 - : Jsont.json = {&quot;foo&quot;:&quot;bar&quot;}</code></pre><p>For arrays, it removes and shifts:</p><pre class="language-ocaml"><code># let three_elem = parse_json {|{&quot;foo&quot;:[&quot;a&quot;,&quot;b&quot;,&quot;c&quot;]}|};; 112 val three_elem : Jsont.json = {&quot;foo&quot;:[&quot;a&quot;,&quot;b&quot;,&quot;c&quot;]} 113 # remove (of_string_nav &quot;/foo/1&quot;) three_elem ;; 114 - - : Jsont.json = {&quot;foo&quot;:[&quot;a&quot;,&quot;c&quot;]}</code></pre><h3 id="replace"><a href="#replace" class="anchor"></a>Replace</h3><p>The <code>replace</code> operation updates an existing value:</p><pre class="language-ocaml"><code># replace (of_string_nav &quot;/foo&quot;) obj ~value:(Jsont.Json.string &quot;baz&quot;) 115 ;; 116 - - : Jsont.json = {&quot;foo&quot;:&quot;baz&quot;}</code></pre><p>Unlike <code>add</code>, <code>replace</code> requires the target to already exist (hence <code>nav t</code>). Attempting to replace a nonexistent path raises an error.</p><h3 id="move"><a href="#move" class="anchor"></a>Move</h3><p>The <code>move</code> operation relocates a value. The source (<code>from</code>) must be a <code>nav t</code> (you can only move something that exists), but the destination (<code>path</code>) accepts <code>any</code>:</p><pre class="language-ocaml"><code># let nested = parse_json {|{&quot;foo&quot;:{&quot;bar&quot;:&quot;baz&quot;},&quot;qux&quot;:{}}|};; 117 val nested : Jsont.json = {&quot;foo&quot;:{&quot;bar&quot;:&quot;baz&quot;},&quot;qux&quot;:{}} 118 # move ~from:(of_string_nav &quot;/foo/bar&quot;) ~path:(of_string &quot;/qux/thud&quot;) nested;; 119 - - : Jsont.json = {&quot;foo&quot;:{},&quot;qux&quot;:{&quot;thud&quot;:&quot;baz&quot;}}</code></pre><h3 id="copy"><a href="#copy" class="anchor"></a>Copy</h3><p>The <code>copy</code> operation duplicates a value (same typing as <code>move</code>):</p><pre class="language-ocaml"><code># let to_copy = parse_json {|{&quot;foo&quot;:{&quot;bar&quot;:&quot;baz&quot;}}|};; 120 val to_copy : Jsont.json = {&quot;foo&quot;:{&quot;bar&quot;:&quot;baz&quot;}} 121 # copy ~from:(of_string_nav &quot;/foo/bar&quot;) ~path:(of_string &quot;/foo/qux&quot;) to_copy;; 122 - - : Jsont.json = {&quot;foo&quot;:{&quot;bar&quot;:&quot;baz&quot;,&quot;qux&quot;:&quot;baz&quot;}}</code></pre><h3 id="test"><a href="#test" class="anchor"></a>Test</h3><p>The <code>test</code> operation verifies a value (useful in JSON Patch):</p><pre class="language-ocaml"><code># test (of_string_nav &quot;/foo&quot;) obj ~expected:(Jsont.Json.string &quot;bar&quot;);; 123 - : bool = true 124 # test (of_string_nav &quot;/foo&quot;) obj ~expected:(Jsont.Json.string &quot;wrong&quot;);; 125 - : bool = false</code></pre><h2 id="escaping"><a href="#escaping" class="anchor"></a>Escaping Special Characters</h2><p><a href="https://datatracker.ietf.org/doc/html/rfc6901#section-3">RFC 6901, Section 3</a> explains the escaping rules:</p><p><i>Because the characters '~' (%x7E) and '/' (%x2F) have special meanings in JSON Pointer, '~' needs to be encoded as '~0' and '/' needs to be encoded as '~1' when these characters appear in a reference token.</i></p><p>Why these specific characters?</p><ul><li><code>/</code> separates tokens, so it must be escaped inside a token</li><li><code>~</code> is the escape character itself, so it must also be escaped</li></ul><p>The escape sequences are:</p><ul><li><code>~0</code> represents <code>~</code> (tilde)</li><li><code>~1</code> represents <code>/</code> (forward slash)</li></ul><h3 id="the-library-handles-escaping-automatically"><a href="#the-library-handles-escaping-automatically" class="anchor"></a>The Library Handles Escaping Automatically</h3><p><b>Important</b>: When using <code>json-pointer</code> programmatically, you rarely need to think about escaping. The <code>Mem</code> variant stores unescaped strings, and escaping happens automatically during serialization:</p><pre class="language-ocaml"><code># let p = make [mem &quot;a/b&quot;];; ··· 127 # to_string p;; 128 - : string = &quot;/a~1b&quot; 129 # of_string_nav &quot;/a~1b&quot;;; 130 - - : nav t = [Mem &quot;a/b&quot;]</code></pre><h3 id="escaping-in-action"><a href="#escaping-in-action" class="anchor"></a>Escaping in Action</h3><p>The <code>Token</code> module exposes the escaping functions:</p><pre class="language-ocaml"><code># Token.escape &quot;hello&quot;;; 131 - : string = &quot;hello&quot; 132 # Token.escape &quot;a/b&quot;;; 133 - : string = &quot;a~1b&quot; ··· 148 - : string = &quot;/%20&quot;</code></pre><p>The <code>%</code> character must be percent-encoded as <code>%25</code> in URIs, and spaces become <code>%20</code>.</p><p>Here's the RFC example showing the URI fragment forms:</p><ul><li><code>&quot;&quot;</code> -&gt; <code>#</code> -&gt; whole document</li><li><code>&quot;/foo&quot;</code> -&gt; <code>#/foo</code> -&gt; <code>[&quot;bar&quot;, &quot;baz&quot;]</code></li><li><code>&quot;/foo/0&quot;</code> -&gt; <code>#/foo/0</code> -&gt; <code>&quot;bar&quot;</code></li><li><code>&quot;/&quot;</code> -&gt; <code>#/</code> -&gt; <code>0</code></li><li><code>&quot;/a~1b&quot;</code> -&gt; <code>#/a~1b</code> -&gt; <code>1</code></li><li><code>&quot;/c%d&quot;</code> -&gt; <code>#/c%25d</code> -&gt; <code>2</code></li><li><code>&quot;/ &quot;</code> -&gt; <code>#/%20</code> -&gt; <code>7</code></li><li><code>&quot;/m~0n&quot;</code> -&gt; <code>#/m~0n</code> -&gt; <code>8</code></li></ul><h2 id="building-pointers-programmatically"><a href="#building-pointers-programmatically" class="anchor"></a>Building Pointers Programmatically</h2><p>Instead of parsing strings, you can build pointers from indices:</p><pre class="language-ocaml"><code># let port_ptr = make [mem &quot;database&quot;; mem &quot;port&quot;];; 149 val port_ptr : nav t = [Mem &quot;database&quot;; Mem &quot;port&quot;] 150 # to_string port_ptr;; 151 - - : string = &quot;/database/port&quot;</code></pre><p>For array access, use the <code>nth</code> helper:</p><pre class="language-ocaml"><code># let first_feature_ptr = make [mem &quot;features&quot;; nth 0];; 152 val first_feature_ptr : nav t = [Mem &quot;features&quot;; Nth 0] 153 # to_string first_feature_ptr;; 154 - - : string = &quot;/features/0&quot;</code></pre><h3 id="pointer-navigation"><a href="#pointer-navigation" class="anchor"></a>Pointer Navigation</h3><p>You can build pointers incrementally using the <code>/</code> operator (or <code>append_index</code>):</p><pre class="language-ocaml"><code># let db_ptr = of_string_nav &quot;/database&quot;;; 155 val db_ptr : nav t = [Mem &quot;database&quot;] 156 # let creds_ptr = db_ptr / mem &quot;credentials&quot;;; 157 val creds_ptr : nav t = [Mem &quot;database&quot;; Mem &quot;credentials&quot;] ··· 172 &quot;features&quot;: [&quot;auth&quot;, &quot;logging&quot;, &quot;metrics&quot;] 173 }|};; 174 val config_json : Jsont.json = 175 - {&quot;database&quot;:{&quot;host&quot;:&quot;localhost&quot;,&quot;port&quot;:5432,&quot;credentials&quot;:{&quot;username&quot;:&quot;admin&quot;,&quot;password&quot;:&quot;secret&quot;}},&quot;features&quot;:[&quot;auth&quot;,&quot;logging&quot;,&quot;metrics&quot;]}</code></pre><h3 id="typed-access-with-path"><a href="#typed-access-with-path" class="anchor"></a>Typed Access with <code>path</code></h3><p>The <code>path</code> combinator combines pointer navigation with typed decoding:</p><pre class="language-ocaml"><code># let nav = of_string_nav &quot;/database/host&quot;;; 176 val nav : nav t = [Mem &quot;database&quot;; Mem &quot;host&quot;] 177 # let db_host = 178 Jsont.Json.decode ··· 221 (path (of_string_nav &quot;/database/port&quot;) Jsont.int) 222 config_json 223 |&gt; Result.get_ok;; 224 - val typed_port : int = 5432</code></pre><p>The typed approach catches mismatches at decode time with clear errors.</p><h3 id="updates-with-polymorphic-pointers"><a href="#updates-with-polymorphic-pointers" class="anchor"></a>Updates with Polymorphic Pointers</h3><p>The <code>set</code> and <code>add</code> functions accept <code>any</code> pointers, which means you can use the result of <code>of_string</code> directly without pattern matching:</p><pre class="language-ocaml"><code># let tasks = parse_json {|{&quot;tasks&quot;:[&quot;buy milk&quot;]}|};; 225 val tasks : Jsont.json = {&quot;tasks&quot;:[&quot;buy milk&quot;]} 226 # set (of_string &quot;/tasks/0&quot;) tasks ~value:(Jsont.Json.string &quot;buy eggs&quot;);; 227 - : Jsont.json = {&quot;tasks&quot;:[&quot;buy eggs&quot;]} 228 # set (of_string &quot;/tasks/-&quot;) tasks ~value:(Jsont.Json.string &quot;call mom&quot;);; 229 - - : Jsont.json = {&quot;tasks&quot;:[&quot;buy milk&quot;,&quot;call mom&quot;]}</code></pre><p>This is useful for implementing JSON Patch (<a href="https://datatracker.ietf.org/doc/html/rfc6902">RFC 6902</a>) where operations like <code>&quot;add&quot;</code> can target either existing positions or the append marker. If you need to distinguish between pointer types at runtime, use <code>of_string_kind</code> which returns a polymorphic variant:</p><pre class="language-ocaml"><code># of_string_kind &quot;/tasks/0&quot;;; 230 - : [ `Append of append t | `Nav of nav t ] = `Nav [Mem &quot;tasks&quot;; Nth 0] 231 # of_string_kind &quot;/tasks/-&quot;;; 232 - : [ `Append of append t | `Nav of nav t ] = `Append [Mem &quot;tasks&quot;] /-</code></pre><h2 id="summary"><a href="#summary" class="anchor"></a>Summary</h2><p>JSON Pointer (<a href="https://datatracker.ietf.org/doc/html/rfc6901">RFC 6901</a>) provides a simple but powerful way to address values within JSON documents:</p><ol><li><b>Syntax</b>: Pointers are strings of <code>/</code>-separated reference tokens</li><li><b>Escaping</b>: Use <code>~0</code> for <code>~</code> and <code>~1</code> for <code>/</code> in tokens (handled automatically by the library)</li><li><b>Evaluation</b>: Tokens navigate through objects (by key) and arrays (by index)</li><li><b>URI Encoding</b>: Pointers can be percent-encoded for use in URIs</li><li><b>Mutations</b>: Combined with JSON Patch (<a href="https://datatracker.ietf.org/doc/html/rfc6902">RFC 6902</a>), pointers enable structured updates</li><li><b>Type Safety</b>: 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></ol><p>The <code>json-pointer</code> library implements all of this with type-safe OCaml interfaces, integration with the <code>jsont</code> codec system, and proper error handling for malformed pointers and missing values.</p><h3 id="key-points-on-json-pointer-vs-json-path"><a href="#key-points-on-json-pointer-vs-json-path" class="anchor"></a>Key Points on JSON Pointer vs JSON Path</h3><ul><li><b>JSON Pointer</b> addresses a <em>single</em> location (like a file path)</li><li><b>JSON Path</b> queries for <em>multiple</em> values (like a search)</li><li>The <code>-</code> token is unique to JSON Pointer - it means &quot;append position&quot; for arrays</li><li>The library uses phantom types to enforce that <code>-</code> (append) pointers cannot be used with <code>get</code>/<code>find</code></li></ul></div></body></html>
··· 44 val rfc_example : Jsont.json = 45 {&quot;foo&quot;:[&quot;bar&quot;,&quot;baz&quot;],&quot;&quot;:0,&quot;a/b&quot;:1,&quot;c%d&quot;:2,&quot;e^f&quot;:3,&quot;g|h&quot;:4,&quot;i\\j&quot;:5,&quot;k\&quot;l&quot;:6,&quot; &quot;:7,&quot;m~n&quot;:8}</code></pre><p>This document is carefully constructed to exercise various edge cases!</p><h3 id="the-root-pointer"><a href="#the-root-pointer" class="anchor"></a>The Root Pointer</h3><pre class="language-ocaml"><code># get root rfc_example ;; 46 - : Jsont.json = 47 + {&quot;foo&quot;:[&quot;bar&quot;,&quot;baz&quot;],&quot;&quot;:0,&quot;a/b&quot;:1,&quot;c%d&quot;:2,&quot;e^f&quot;:3,&quot;g|h&quot;:4,&quot;i\\j&quot;:5,&quot;k\&quot;l&quot;:6,&quot; &quot;:7,&quot;m~n&quot;:8}</code></pre><p>The empty pointer (<code>Json_pointer.root</code>) returns the whole document.</p><h3 id="object-member-access"><a href="#object-member-access" class="anchor"></a>Object Member Access</h3><pre class="language-ocaml"><code># get (of_string_nav &quot;/foo&quot;) rfc_example ;; 48 - : Jsont.json = [&quot;bar&quot;,&quot;baz&quot;]</code></pre><p><code>/foo</code> accesses the member named <code>foo</code>, which is an array.</p><h3 id="array-index-access"><a href="#array-index-access" class="anchor"></a>Array Index Access</h3><pre class="language-ocaml"><code># get (of_string_nav &quot;/foo/0&quot;) rfc_example ;; 49 - : Jsont.json = &quot;bar&quot; 50 # get (of_string_nav &quot;/foo/1&quot;) rfc_example ;; ··· 77 - : nav t = [Mem &quot;foo&quot;; Mem &quot;01&quot;]</code></pre><p>But <code>01</code> has a leading zero, so it's NOT treated as an array index - it becomes a member name instead. This protects against accidental octal interpretation.</p><h2 id="the-end-of-array-marker:---and-type-safety"><a href="#the-end-of-array-marker:---and-type-safety" class="anchor"></a>The End-of-Array Marker: <code>-</code> and Type Safety</h2><p><a href="https://datatracker.ietf.org/doc/html/rfc6901#section-4">RFC 6901, Section 4</a> introduces a special token:</p><p><i>exactly the single character &quot;-&quot;, making the new referenced value the (nonexistent) member after the last array element.</i></p><p>This <code>-</code> marker is unique to JSON Pointer (JSON Path has no equivalent). It's primarily useful for JSON Patch operations (<a href="https://datatracker.ietf.org/doc/html/rfc6902">RFC 6902</a>) to append elements to arrays.</p><h3 id="navigation-vs-append-pointers"><a href="#navigation-vs-append-pointers" class="anchor"></a>Navigation vs Append Pointers</h3><p>The <code>json-pointer</code> library uses <b>phantom types</b> to encode the difference between pointers that can be used for navigation and pointers that target the &quot;append position&quot;:</p><pre>type nav (* A pointer to an existing element *) 78 type append (* A pointer ending with &quot;-&quot; (append position) *) 79 type 'a t (* Pointer with phantom type parameter *) 80 + type any (* Existential: wraps either nav or append *)</pre><p>When you parse a pointer with <code>Json_pointer.of_string</code>, you get an <code>Json_pointer.any</code> pointer that can be used directly with mutation operations:</p><pre class="language-ocaml"><code># of_string &quot;/foo/0&quot;;; 81 - : any = Any &lt;abstr&gt; 82 # of_string &quot;/foo/-&quot;;; 83 + - : any = Any &lt;abstr&gt;</code></pre><p>The <code>-</code> creates an append pointer. The <code>Json_pointer.any</code> type wraps either kind, making it ergonomic to use with operations like <code>Json_pointer.set</code> and <code>Json_pointer.add</code>.</p><h3 id="why-two-pointer-types?"><a href="#why-two-pointer-types?" class="anchor"></a>Why Two Pointer Types?</h3><p>The RFC explains that <code>-</code> refers to a <em>nonexistent</em> position:</p><p><i>Note that the use of the &quot;-&quot; character to index an array will always result in such an error condition because by definition it refers to a nonexistent array element.</i></p><p>So you <b>cannot use <code>get</code> or <code>find</code></b> with an append pointer - it makes no sense to retrieve a value from a position that doesn't exist! The library enforces this:</p><ul><li>Use <code>Json_pointer.of_string_nav</code> when you need to call <code>Json_pointer.get</code> or <code>Json_pointer.find</code></li><li>Use <code>Json_pointer.of_string</code> (returns <code>Json_pointer.any</code>) for mutation operations</li></ul><p>Mutation operations like <code>Json_pointer.add</code> accept <code>Json_pointer.any</code> directly:</p><pre class="language-ocaml"><code># let arr_obj = parse_json {|{&quot;foo&quot;:[&quot;a&quot;,&quot;b&quot;]}|};; 84 val arr_obj : Jsont.json = {&quot;foo&quot;:[&quot;a&quot;,&quot;b&quot;]} 85 # add (of_string &quot;/foo/-&quot;) arr_obj ~value:(Jsont.Json.string &quot;c&quot;);; 86 + - : Jsont.json = {&quot;foo&quot;:[&quot;a&quot;,&quot;b&quot;,&quot;c&quot;]}</code></pre><p>For retrieval operations, use <code>Json_pointer.of_string_nav</code> which ensures the pointer doesn't contain <code>-</code>:</p><pre class="language-ocaml"><code># of_string_nav &quot;/foo/0&quot;;; 87 - : nav t = [Mem &quot;foo&quot;; Nth 0] 88 # of_string_nav &quot;/foo/-&quot;;; 89 Exception: 90 + Jsont.Error Invalid JSON Pointer: '-' not allowed in navigation pointer.</code></pre><h3 id="creating-append-pointers-programmatically"><a href="#creating-append-pointers-programmatically" class="anchor"></a>Creating Append Pointers Programmatically</h3><p>You can convert a navigation pointer to an append pointer using <code>Json_pointer.at_end</code>:</p><pre class="language-ocaml"><code># let nav_ptr = of_string_nav &quot;/foo&quot;;; 91 val nav_ptr : nav t = [Mem &quot;foo&quot;] 92 # let app_ptr = at_end nav_ptr;; 93 val app_ptr : append t = [Mem &quot;foo&quot;] /- 94 # to_string app_ptr;; 95 + - : string = &quot;/foo/-&quot;</code></pre><h2 id="mutation-operations"><a href="#mutation-operations" class="anchor"></a>Mutation Operations</h2><p>While <a href="https://datatracker.ietf.org/doc/html/rfc6901">RFC 6901</a> defines JSON Pointer for read-only access, <a href="https://datatracker.ietf.org/doc/html/rfc6902">RFC 6902</a> (JSON Patch) uses JSON Pointer for modifications. The <code>json-pointer</code> library provides these operations.</p><h3 id="add"><a href="#add" class="anchor"></a>Add</h3><p>The <code>Json_pointer.add</code> operation inserts a value at a location. It accepts <code>Json_pointer.any</code> pointers, so you can use <code>Json_pointer.of_string</code> directly:</p><pre class="language-ocaml"><code># let obj = parse_json {|{&quot;foo&quot;:&quot;bar&quot;}|};; 96 val obj : Jsont.json = {&quot;foo&quot;:&quot;bar&quot;} 97 # add (of_string &quot;/baz&quot;) obj ~value:(Jsont.Json.string &quot;qux&quot;);; 98 + - : Jsont.json = {&quot;foo&quot;:&quot;bar&quot;,&quot;baz&quot;:&quot;qux&quot;}</code></pre><p>For arrays, <code>Json_pointer.add</code> inserts BEFORE the specified index:</p><pre class="language-ocaml"><code># let arr_obj = parse_json {|{&quot;foo&quot;:[&quot;a&quot;,&quot;b&quot;]}|};; 99 val arr_obj : Jsont.json = {&quot;foo&quot;:[&quot;a&quot;,&quot;b&quot;]} 100 # add (of_string &quot;/foo/1&quot;) arr_obj ~value:(Jsont.Json.string &quot;X&quot;);; 101 - : Jsont.json = {&quot;foo&quot;:[&quot;a&quot;,&quot;X&quot;,&quot;b&quot;]}</code></pre><p>This is where the <code>-</code> marker shines - it appends to the end:</p><pre class="language-ocaml"><code># add (of_string &quot;/foo/-&quot;) arr_obj ~value:(Jsont.Json.string &quot;c&quot;);; 102 + - : Jsont.json = {&quot;foo&quot;:[&quot;a&quot;,&quot;b&quot;,&quot;c&quot;]}</code></pre><p>You can also use <code>Json_pointer.at_end</code> to create an append pointer programmatically:</p><pre class="language-ocaml"><code># add (any (at_end (of_string_nav &quot;/foo&quot;))) arr_obj ~value:(Jsont.Json.string &quot;c&quot;);; 103 + - : Jsont.json = {&quot;foo&quot;:[&quot;a&quot;,&quot;b&quot;,&quot;c&quot;]}</code></pre><h3 id="ergonomic-mutation-with-any"><a href="#ergonomic-mutation-with-any" class="anchor"></a>Ergonomic Mutation with <code>any</code></h3><p>Since <code>Json_pointer.add</code>, <code>Json_pointer.set</code>, <code>Json_pointer.move</code>, and <code>Json_pointer.copy</code> accept <code>Json_pointer.any</code> pointers, you can use <code>Json_pointer.of_string</code> directly without any pattern matching. This makes JSON Patch implementations straightforward:</p><pre class="language-ocaml"><code># let items = parse_json {|{&quot;items&quot;:[&quot;x&quot;]}|};; 104 val items : Jsont.json = {&quot;items&quot;:[&quot;x&quot;]} 105 # add (of_string &quot;/items/0&quot;) items ~value:(Jsont.Json.string &quot;y&quot;);; 106 - : Jsont.json = {&quot;items&quot;:[&quot;y&quot;,&quot;x&quot;]} 107 # add (of_string &quot;/items/-&quot;) items ~value:(Jsont.Json.string &quot;z&quot;);; 108 + - : Jsont.json = {&quot;items&quot;:[&quot;x&quot;,&quot;z&quot;]}</code></pre><p>The same pointer works whether it targets an existing position or the append marker - no conditional logic needed.</p><h3 id="remove"><a href="#remove" class="anchor"></a>Remove</h3><p>The <code>Json_pointer.remove</code> operation deletes a value. It only accepts <code>nav t</code> because you can only remove something that exists:</p><pre class="language-ocaml"><code># let two_fields = parse_json {|{&quot;foo&quot;:&quot;bar&quot;,&quot;baz&quot;:&quot;qux&quot;}|};; 109 val two_fields : Jsont.json = {&quot;foo&quot;:&quot;bar&quot;,&quot;baz&quot;:&quot;qux&quot;} 110 # remove (of_string_nav &quot;/baz&quot;) two_fields ;; 111 - : Jsont.json = {&quot;foo&quot;:&quot;bar&quot;}</code></pre><p>For arrays, it removes and shifts:</p><pre class="language-ocaml"><code># let three_elem = parse_json {|{&quot;foo&quot;:[&quot;a&quot;,&quot;b&quot;,&quot;c&quot;]}|};; 112 val three_elem : Jsont.json = {&quot;foo&quot;:[&quot;a&quot;,&quot;b&quot;,&quot;c&quot;]} 113 # remove (of_string_nav &quot;/foo/1&quot;) three_elem ;; 114 + - : Jsont.json = {&quot;foo&quot;:[&quot;a&quot;,&quot;c&quot;]}</code></pre><h3 id="replace"><a href="#replace" class="anchor"></a>Replace</h3><p>The <code>Json_pointer.replace</code> operation updates an existing value:</p><pre class="language-ocaml"><code># replace (of_string_nav &quot;/foo&quot;) obj ~value:(Jsont.Json.string &quot;baz&quot;) 115 ;; 116 + - : Jsont.json = {&quot;foo&quot;:&quot;baz&quot;}</code></pre><p>Unlike <code>Json_pointer.add</code>, <code>Json_pointer.replace</code> requires the target to already exist (hence <code>nav t</code>). Attempting to replace a nonexistent path raises an error.</p><h3 id="move"><a href="#move" class="anchor"></a>Move</h3><p>The <code>Json_pointer.move</code> operation relocates a value. The source (<code>from</code>) must be a <code>nav t</code> (you can only move something that exists), but the destination (<code>path</code>) accepts <code>Json_pointer.any</code>:</p><pre class="language-ocaml"><code># let nested = parse_json {|{&quot;foo&quot;:{&quot;bar&quot;:&quot;baz&quot;},&quot;qux&quot;:{}}|};; 117 val nested : Jsont.json = {&quot;foo&quot;:{&quot;bar&quot;:&quot;baz&quot;},&quot;qux&quot;:{}} 118 # move ~from:(of_string_nav &quot;/foo/bar&quot;) ~path:(of_string &quot;/qux/thud&quot;) nested;; 119 + - : Jsont.json = {&quot;foo&quot;:{},&quot;qux&quot;:{&quot;thud&quot;:&quot;baz&quot;}}</code></pre><h3 id="copy"><a href="#copy" class="anchor"></a>Copy</h3><p>The <code>Json_pointer.copy</code> operation duplicates a value (same typing as <code>Json_pointer.move</code>):</p><pre class="language-ocaml"><code># let to_copy = parse_json {|{&quot;foo&quot;:{&quot;bar&quot;:&quot;baz&quot;}}|};; 120 val to_copy : Jsont.json = {&quot;foo&quot;:{&quot;bar&quot;:&quot;baz&quot;}} 121 # copy ~from:(of_string_nav &quot;/foo/bar&quot;) ~path:(of_string &quot;/foo/qux&quot;) to_copy;; 122 + - : Jsont.json = {&quot;foo&quot;:{&quot;bar&quot;:&quot;baz&quot;,&quot;qux&quot;:&quot;baz&quot;}}</code></pre><h3 id="test"><a href="#test" class="anchor"></a>Test</h3><p>The <code>Json_pointer.test</code> operation verifies a value (useful in JSON Patch):</p><pre class="language-ocaml"><code># test (of_string_nav &quot;/foo&quot;) obj ~expected:(Jsont.Json.string &quot;bar&quot;);; 123 - : bool = true 124 # test (of_string_nav &quot;/foo&quot;) obj ~expected:(Jsont.Json.string &quot;wrong&quot;);; 125 - : bool = false</code></pre><h2 id="escaping"><a href="#escaping" class="anchor"></a>Escaping Special Characters</h2><p><a href="https://datatracker.ietf.org/doc/html/rfc6901#section-3">RFC 6901, Section 3</a> explains the escaping rules:</p><p><i>Because the characters '~' (%x7E) and '/' (%x2F) have special meanings in JSON Pointer, '~' needs to be encoded as '~0' and '/' needs to be encoded as '~1' when these characters appear in a reference token.</i></p><p>Why these specific characters?</p><ul><li><code>/</code> separates tokens, so it must be escaped inside a token</li><li><code>~</code> is the escape character itself, so it must also be escaped</li></ul><p>The escape sequences are:</p><ul><li><code>~0</code> represents <code>~</code> (tilde)</li><li><code>~1</code> represents <code>/</code> (forward slash)</li></ul><h3 id="the-library-handles-escaping-automatically"><a href="#the-library-handles-escaping-automatically" class="anchor"></a>The Library Handles Escaping Automatically</h3><p><b>Important</b>: When using <code>json-pointer</code> programmatically, you rarely need to think about escaping. The <code>Mem</code> variant stores unescaped strings, and escaping happens automatically during serialization:</p><pre class="language-ocaml"><code># let p = make [mem &quot;a/b&quot;];; ··· 127 # to_string p;; 128 - : string = &quot;/a~1b&quot; 129 # of_string_nav &quot;/a~1b&quot;;; 130 + - : nav t = [Mem &quot;a/b&quot;]</code></pre><h3 id="escaping-in-action"><a href="#escaping-in-action" class="anchor"></a>Escaping in Action</h3><p>The <code>Json_pointer.Token</code> module exposes the escaping functions:</p><pre class="language-ocaml"><code># Token.escape &quot;hello&quot;;; 131 - : string = &quot;hello&quot; 132 # Token.escape &quot;a/b&quot;;; 133 - : string = &quot;a~1b&quot; ··· 148 - : string = &quot;/%20&quot;</code></pre><p>The <code>%</code> character must be percent-encoded as <code>%25</code> in URIs, and spaces become <code>%20</code>.</p><p>Here's the RFC example showing the URI fragment forms:</p><ul><li><code>&quot;&quot;</code> -&gt; <code>#</code> -&gt; whole document</li><li><code>&quot;/foo&quot;</code> -&gt; <code>#/foo</code> -&gt; <code>[&quot;bar&quot;, &quot;baz&quot;]</code></li><li><code>&quot;/foo/0&quot;</code> -&gt; <code>#/foo/0</code> -&gt; <code>&quot;bar&quot;</code></li><li><code>&quot;/&quot;</code> -&gt; <code>#/</code> -&gt; <code>0</code></li><li><code>&quot;/a~1b&quot;</code> -&gt; <code>#/a~1b</code> -&gt; <code>1</code></li><li><code>&quot;/c%d&quot;</code> -&gt; <code>#/c%25d</code> -&gt; <code>2</code></li><li><code>&quot;/ &quot;</code> -&gt; <code>#/%20</code> -&gt; <code>7</code></li><li><code>&quot;/m~0n&quot;</code> -&gt; <code>#/m~0n</code> -&gt; <code>8</code></li></ul><h2 id="building-pointers-programmatically"><a href="#building-pointers-programmatically" class="anchor"></a>Building Pointers Programmatically</h2><p>Instead of parsing strings, you can build pointers from indices:</p><pre class="language-ocaml"><code># let port_ptr = make [mem &quot;database&quot;; mem &quot;port&quot;];; 149 val port_ptr : nav t = [Mem &quot;database&quot;; Mem &quot;port&quot;] 150 # to_string port_ptr;; 151 + - : string = &quot;/database/port&quot;</code></pre><p>For array access, use the <code>Json_pointer.nth</code> helper:</p><pre class="language-ocaml"><code># let first_feature_ptr = make [mem &quot;features&quot;; nth 0];; 152 val first_feature_ptr : nav t = [Mem &quot;features&quot;; Nth 0] 153 # to_string first_feature_ptr;; 154 + - : string = &quot;/features/0&quot;</code></pre><h3 id="pointer-navigation"><a href="#pointer-navigation" class="anchor"></a>Pointer Navigation</h3><p>You can build pointers incrementally using the <code>/</code> operator (or <code>Json_pointer.append_index</code>):</p><pre class="language-ocaml"><code># let db_ptr = of_string_nav &quot;/database&quot;;; 155 val db_ptr : nav t = [Mem &quot;database&quot;] 156 # let creds_ptr = db_ptr / mem &quot;credentials&quot;;; 157 val creds_ptr : nav t = [Mem &quot;database&quot;; Mem &quot;credentials&quot;] ··· 172 &quot;features&quot;: [&quot;auth&quot;, &quot;logging&quot;, &quot;metrics&quot;] 173 }|};; 174 val config_json : Jsont.json = 175 + {&quot;database&quot;:{&quot;host&quot;:&quot;localhost&quot;,&quot;port&quot;:5432,&quot;credentials&quot;:{&quot;username&quot;:&quot;admin&quot;,&quot;password&quot;:&quot;secret&quot;}},&quot;features&quot;:[&quot;auth&quot;,&quot;logging&quot;,&quot;metrics&quot;]}</code></pre><h3 id="typed-access-with-path"><a href="#typed-access-with-path" class="anchor"></a>Typed Access with <code>path</code></h3><p>The <code>Json_pointer.path</code> combinator combines pointer navigation with typed decoding:</p><pre class="language-ocaml"><code># let nav = of_string_nav &quot;/database/host&quot;;; 176 val nav : nav t = [Mem &quot;database&quot;; Mem &quot;host&quot;] 177 # let db_host = 178 Jsont.Json.decode ··· 221 (path (of_string_nav &quot;/database/port&quot;) Jsont.int) 222 config_json 223 |&gt; Result.get_ok;; 224 + val typed_port : int = 5432</code></pre><p>The typed approach catches mismatches at decode time with clear errors.</p><h3 id="updates-with-polymorphic-pointers"><a href="#updates-with-polymorphic-pointers" class="anchor"></a>Updates with Polymorphic Pointers</h3><p>The <code>Json_pointer.set</code> and <code>Json_pointer.add</code> functions accept <code>Json_pointer.any</code> pointers, which means you can use the result of <code>Json_pointer.of_string</code> directly without pattern matching:</p><pre class="language-ocaml"><code># let tasks = parse_json {|{&quot;tasks&quot;:[&quot;buy milk&quot;]}|};; 225 val tasks : Jsont.json = {&quot;tasks&quot;:[&quot;buy milk&quot;]} 226 # set (of_string &quot;/tasks/0&quot;) tasks ~value:(Jsont.Json.string &quot;buy eggs&quot;);; 227 - : Jsont.json = {&quot;tasks&quot;:[&quot;buy eggs&quot;]} 228 # set (of_string &quot;/tasks/-&quot;) tasks ~value:(Jsont.Json.string &quot;call mom&quot;);; 229 + - : Jsont.json = {&quot;tasks&quot;:[&quot;buy milk&quot;,&quot;call mom&quot;]}</code></pre><p>This is useful for implementing JSON Patch (<a href="https://datatracker.ietf.org/doc/html/rfc6902">RFC 6902</a>) where operations like <code>&quot;add&quot;</code> can target either existing positions or the append marker. If you need to distinguish between pointer types at runtime, use <code>Json_pointer.of_string_kind</code> which returns a polymorphic variant:</p><pre class="language-ocaml"><code># of_string_kind &quot;/tasks/0&quot;;; 230 - : [ `Append of append t | `Nav of nav t ] = `Nav [Mem &quot;tasks&quot;; Nth 0] 231 # of_string_kind &quot;/tasks/-&quot;;; 232 - : [ `Append of append t | `Nav of nav t ] = `Append [Mem &quot;tasks&quot;] /-</code></pre><h2 id="summary"><a href="#summary" class="anchor"></a>Summary</h2><p>JSON Pointer (<a href="https://datatracker.ietf.org/doc/html/rfc6901">RFC 6901</a>) provides a simple but powerful way to address values within JSON documents:</p><ol><li><b>Syntax</b>: Pointers are strings of <code>/</code>-separated reference tokens</li><li><b>Escaping</b>: Use <code>~0</code> for <code>~</code> and <code>~1</code> for <code>/</code> in tokens (handled automatically by the library)</li><li><b>Evaluation</b>: Tokens navigate through objects (by key) and arrays (by index)</li><li><b>URI Encoding</b>: Pointers can be percent-encoded for use in URIs</li><li><b>Mutations</b>: Combined with JSON Patch (<a href="https://datatracker.ietf.org/doc/html/rfc6902">RFC 6902</a>), pointers enable structured updates</li><li><b>Type Safety</b>: 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></ol><p>The <code>json-pointer</code> library implements all of this with type-safe OCaml interfaces, integration with the <code>jsont</code> codec system, and proper error handling for malformed pointers and missing values.</p><h3 id="key-points-on-json-pointer-vs-json-path"><a href="#key-points-on-json-pointer-vs-json-path" class="anchor"></a>Key Points on JSON Pointer vs JSON Path</h3><ul><li><b>JSON Pointer</b> addresses a <em>single</em> location (like a file path)</li><li><b>JSON Path</b> queries for <em>multiple</em> values (like a search)</li><li>The <code>-</code> token is unique to JSON Pointer - it means &quot;append position&quot; for arrays</li><li>The library uses phantom types to enforce that <code>-</code> (append) pointers cannot be used with <code>get</code>/<code>find</code></li></ul></div></body></html>
+29 -29
doc/tutorial.mld
··· 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 208 - The empty pointer ({!root}) returns the whole document. 209 210 {2 Object Member Access} 211 ··· 376 type any (* Existential: wraps either nav or append *) 377 v} 378 379 - When you parse a pointer with {!of_string}, you get an {!any} pointer 380 that can be used directly with mutation operations: 381 382 {@ocaml[ ··· 386 - : any = Any <abstr> 387 ]} 388 389 - The [-] creates an append pointer. The {!any} type wraps either kind, 390 - making it ergonomic to use with operations like {!set} and {!add}. 391 392 {2 Why Two Pointer Types?} 393 ··· 400 So you {b cannot use [get] or [find]} with an append pointer - it makes 401 no sense to retrieve a value from a position that doesn't exist! The 402 library enforces this: 403 - - Use {!of_string_nav} when you need to call {!get} or {!find} 404 - - Use {!of_string} (returns {!any}) for mutation operations 405 406 - Mutation operations like {!add} accept {!any} directly: 407 408 {x@ocaml[ 409 # let arr_obj = parse_json {|{"foo":["a","b"]}|};; ··· 412 - : Jsont.json = {"foo":["a","b","c"]} 413 ]x} 414 415 - For retrieval operations, use {!of_string_nav} which ensures the pointer 416 doesn't contain [-]: 417 418 {@ocaml[ ··· 425 426 {2 Creating Append Pointers Programmatically} 427 428 - You can convert a navigation pointer to an append pointer using {!at_end}: 429 430 {@ocaml[ 431 # let nav_ptr = of_string_nav "/foo";; ··· 444 445 {2 Add} 446 447 - The {!add} operation inserts a value at a location. It accepts {!any} 448 - pointers, so you can use {!of_string} directly: 449 450 {x@ocaml[ 451 # let obj = parse_json {|{"foo":"bar"}|};; ··· 454 - : Jsont.json = {"foo":"bar","baz":"qux"} 455 ]x} 456 457 - For arrays, {!add} inserts BEFORE the specified index: 458 459 {x@ocaml[ 460 # let arr_obj = parse_json {|{"foo":["a","b"]}|};; ··· 470 - : Jsont.json = {"foo":["a","b","c"]} 471 ]x} 472 473 - You can also use {!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");; ··· 479 480 {2 Ergonomic Mutation with [any]} 481 482 - Since {!add}, {!set}, {!move}, and {!copy} accept {!any} pointers, you can 483 - use {!of_string} directly without any pattern matching. This makes JSON 484 Patch implementations straightforward: 485 486 {x@ocaml[ ··· 497 498 {2 Remove} 499 500 - The {!remove} operation deletes a value. It only accepts [nav t] because 501 you can only remove something that exists: 502 503 {x@ocaml[ ··· 518 519 {2 Replace} 520 521 - The {!replace} operation updates an existing value: 522 523 {@ocaml[ 524 # replace (of_string_nav "/foo") obj ~value:(Jsont.Json.string "baz") ··· 526 - : Jsont.json = {"foo":"baz"} 527 ]} 528 529 - Unlike {!add}, {!replace} requires the target to already exist (hence [nav t]). 530 Attempting to replace a nonexistent path raises an error. 531 532 {2 Move} 533 534 - The {!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]) 536 - accepts {!any}: 537 538 {x@ocaml[ 539 # let nested = parse_json {|{"foo":{"bar":"baz"},"qux":{}}|};; ··· 544 545 {2 Copy} 546 547 - The {!copy} operation duplicates a value (same typing as {!move}): 548 549 {x@ocaml[ 550 # let to_copy = parse_json {|{"foo":{"bar":"baz"}}|};; ··· 555 556 {2 Test} 557 558 - The {!test} operation verifies a value (useful in JSON Patch): 559 560 {@ocaml[ 561 # test (of_string_nav "/foo") obj ~expected:(Jsont.Json.string "bar");; ··· 597 598 {2 Escaping in Action} 599 600 - The {!Token} module exposes the escaping functions: 601 602 {@ocaml[ 603 # Token.escape "hello";; ··· 692 - : string = "/database/port" 693 ]} 694 695 - For array access, use the {!nth} helper: 696 697 {@ocaml[ 698 # let first_feature_ptr = make [mem "features"; nth 0];; ··· 703 704 {2 Pointer Navigation} 705 706 - You can build pointers incrementally using the [/] operator (or {!append_index}): 707 708 {@ocaml[ 709 # let db_ptr = of_string_nav "/database";; ··· 749 750 {2 Typed Access with [path]} 751 752 - The {!path} combinator combines pointer navigation with typed decoding: 753 754 {@ocaml[ 755 # let nav = of_string_nav "/database/host";; ··· 844 845 {2 Updates with Polymorphic Pointers} 846 847 - The {!set} and {!add} functions accept {!any} pointers, which means you can 848 - use the result of {!of_string} directly without pattern matching: 849 850 {x@ocaml[ 851 # let tasks = parse_json {|{"tasks":["buy milk"]}|};; ··· 859 This is useful for implementing JSON Patch ({{:https://datatracker.ietf.org/doc/html/rfc6902}RFC 6902}) where 860 operations like ["add"] can target either existing positions or the 861 append marker. If you need to distinguish between pointer types at runtime, 862 - use {!of_string_kind} which returns a polymorphic variant: 863 864 {x@ocaml[ 865 # of_string_kind "/tasks/0";;
··· 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 208 + The empty pointer ({!Json_pointer.root}) returns the whole document. 209 210 {2 Object Member Access} 211 ··· 376 type any (* Existential: wraps either nav or append *) 377 v} 378 379 + When you parse a pointer with {!Json_pointer.of_string}, you get an {!type:Json_pointer.any} pointer 380 that can be used directly with mutation operations: 381 382 {@ocaml[ ··· 386 - : any = Any <abstr> 387 ]} 388 389 + The [-] creates an append pointer. The {!type:Json_pointer.any} type wraps either kind, 390 + making it ergonomic to use with operations like {!Json_pointer.set} and {!Json_pointer.add}. 391 392 {2 Why Two Pointer Types?} 393 ··· 400 So you {b cannot use [get] or [find]} with an append pointer - it makes 401 no sense to retrieve a value from a position that doesn't exist! The 402 library 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 406 + Mutation operations like {!Json_pointer.add} accept {!type:Json_pointer.any} directly: 407 408 {x@ocaml[ 409 # let arr_obj = parse_json {|{"foo":["a","b"]}|};; ··· 412 - : Jsont.json = {"foo":["a","b","c"]} 413 ]x} 414 415 + For retrieval operations, use {!Json_pointer.of_string_nav} which ensures the pointer 416 doesn't contain [-]: 417 418 {@ocaml[ ··· 425 426 {2 Creating Append Pointers Programmatically} 427 428 + You 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";; ··· 444 445 {2 Add} 446 447 + The {!Json_pointer.add} operation inserts a value at a location. It accepts {!type:Json_pointer.any} 448 + pointers, so you can use {!Json_pointer.of_string} directly: 449 450 {x@ocaml[ 451 # let obj = parse_json {|{"foo":"bar"}|};; ··· 454 - : Jsont.json = {"foo":"bar","baz":"qux"} 455 ]x} 456 457 + For arrays, {!Json_pointer.add} inserts BEFORE the specified index: 458 459 {x@ocaml[ 460 # let arr_obj = parse_json {|{"foo":["a","b"]}|};; ··· 470 - : Jsont.json = {"foo":["a","b","c"]} 471 ]x} 472 473 + You 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");; ··· 479 480 {2 Ergonomic Mutation with [any]} 481 482 + Since {!Json_pointer.add}, {!Json_pointer.set}, {!Json_pointer.move}, and {!Json_pointer.copy} accept {!type:Json_pointer.any} pointers, you can 483 + use {!Json_pointer.of_string} directly without any pattern matching. This makes JSON 484 Patch implementations straightforward: 485 486 {x@ocaml[ ··· 497 498 {2 Remove} 499 500 + The {!Json_pointer.remove} operation deletes a value. It only accepts [nav t] because 501 you can only remove something that exists: 502 503 {x@ocaml[ ··· 518 519 {2 Replace} 520 521 + The {!Json_pointer.replace} operation updates an existing value: 522 523 {@ocaml[ 524 # replace (of_string_nav "/foo") obj ~value:(Jsont.Json.string "baz") ··· 526 - : Jsont.json = {"foo":"baz"} 527 ]} 528 529 + Unlike {!Json_pointer.add}, {!Json_pointer.replace} requires the target to already exist (hence [nav t]). 530 Attempting to replace a nonexistent path raises an error. 531 532 {2 Move} 533 534 + The {!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]) 536 + accepts {!type:Json_pointer.any}: 537 538 {x@ocaml[ 539 # let nested = parse_json {|{"foo":{"bar":"baz"},"qux":{}}|};; ··· 544 545 {2 Copy} 546 547 + The {!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"}}|};; ··· 555 556 {2 Test} 557 558 + The {!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");; ··· 597 598 {2 Escaping in Action} 599 600 + The {!Json_pointer.Token} module exposes the escaping functions: 601 602 {@ocaml[ 603 # Token.escape "hello";; ··· 692 - : string = "/database/port" 693 ]} 694 695 + For array access, use the {!Json_pointer.nth} helper: 696 697 {@ocaml[ 698 # let first_feature_ptr = make [mem "features"; nth 0];; ··· 703 704 {2 Pointer Navigation} 705 706 + You can build pointers incrementally using the [/] operator (or {!Json_pointer.append_index}): 707 708 {@ocaml[ 709 # let db_ptr = of_string_nav "/database";; ··· 749 750 {2 Typed Access with [path]} 751 752 + The {!Json_pointer.path} combinator combines pointer navigation with typed decoding: 753 754 {@ocaml[ 755 # let nav = of_string_nav "/database/host";; ··· 844 845 {2 Updates with Polymorphic Pointers} 846 847 + The {!Json_pointer.set} and {!Json_pointer.add} functions accept {!type:Json_pointer.any} pointers, which means you can 848 + use the result of {!Json_pointer.of_string} directly without pattern matching: 849 850 {x@ocaml[ 851 # let tasks = parse_json {|{"tasks":["buy milk"]}|};; ··· 859 This is useful for implementing JSON Patch ({{:https://datatracker.ietf.org/doc/html/rfc6902}RFC 6902}) where 860 operations like ["add"] can target either existing positions or the 861 append marker. If you need to distinguish between pointer types at runtime, 862 + use {!Json_pointer.of_string_kind} which returns a polymorphic variant: 863 864 {x@ocaml[ 865 # of_string_kind "/tasks/0";;
+26 -26
src/json_pointer.mli
··· 77 (** [unescape s] unescapes a JSON Pointer reference token. 78 Specifically, [~1] becomes [/] and [~0] becomes [~]. 79 80 - @raise Jsont.Error if [s] contains invalid escape sequences 81 (a [~] not followed by [0] or [1]). *) 82 end 83 ··· 137 138 (** {2 Existential wrapper} 139 140 - The {!any} type wraps a pointer of unknown phantom type, allowing 141 ergonomic use with mutation operations like {!set} and {!add} without 142 needing to pattern match on the pointer kind. *) 143 ··· 188 (** {2:coercion Coercion and inspection} *) 189 190 val any : _ t -> any 191 - (** [any p] wraps a typed pointer in the existential {!any} type. 192 - Use this when you have a [nav t] or [append t] but need an {!any} 193 for use with functions like {!set} or {!add}. *) 194 195 val is_nav : any -> bool ··· 202 203 val to_nav_exn : any -> nav t 204 (** [to_nav_exn p] returns the navigation pointer if [p] is one. 205 - @raise Jsont.Error if [p] is an append pointer. *) 206 207 (** {2:parsing Parsing} *) 208 209 val of_string : string -> any 210 (** [of_string s] parses a JSON Pointer from its string representation. 211 212 - Returns an {!any} pointer that can be used directly with mutation 213 operations like {!set} and {!add}. For retrieval operations like 214 {!get}, use {!of_string_nav} instead. 215 ··· 217 with [/]. Each segment between [/] characters is unescaped as a 218 reference token. 219 220 - @raise Jsont.Error if [s] has invalid syntax: 221 - Non-empty string not starting with [/] 222 - Invalid escape sequence ([~] not followed by [0] or [1]) 223 - [-] appears in non-final position *) ··· 230 differently, or when you need a typed pointer for operations that 231 require a specific kind. 232 233 - @raise Jsont.Error if [s] has invalid syntax. *) 234 235 val of_string_nav : string -> nav t 236 (** [of_string_nav s] parses a JSON Pointer that must not contain [-]. ··· 238 Use this when you need a {!nav} pointer for retrieval operations 239 like {!get} or {!find}. 240 241 - @raise Jsont.Error if [s] has invalid syntax or contains [-]. *) 242 243 val of_string_result : string -> (any, string) result 244 (** [of_string_result s] is like {!of_string} but returns a result ··· 251 according to {{:https://www.rfc-editor.org/rfc/rfc3986}RFC 3986}. 252 The leading [#] should {b not} be included in [s]. 253 254 - @raise Jsont.Error on invalid syntax or invalid percent-encoding. *) 255 256 val of_uri_fragment_nav : string -> nav t 257 (** [of_uri_fragment_nav s] is like {!of_uri_fragment} but requires 258 the pointer to not contain [-]. 259 260 - @raise Jsont.Error if invalid or contains [-]. *) 261 262 val of_uri_fragment_result : string -> (any, string) result 263 (** [of_uri_fragment_result s] is like {!of_uri_fragment} but returns ··· 310 311 (** {1 Evaluation} 312 313 - These functions evaluate a JSON Pointer against a {!Jsont.json} value 314 to retrieve the referenced value. They only accept {!nav} pointers 315 since {!append} pointers refer to nonexistent positions. *) 316 317 val get : nav t -> Jsont.json -> Jsont.json 318 (** [get p json] retrieves the value at pointer [p] in [json]. 319 320 - @raise Jsont.Error if: 321 - The pointer references a nonexistent object member 322 - The pointer references an out-of-bounds array index 323 - An index type doesn't match the JSON value (e.g., [Nth] ··· 332 333 (** {1 Mutation} 334 335 - These functions modify a {!Jsont.json} value at a location specified 336 by a JSON Pointer. They are designed to support 337 {{:https://www.rfc-editor.org/rfc/rfc6902}RFC 6902 JSON Patch} 338 operations. ··· 341 applied; they do not mutate the input. 342 343 Functions that support the [-] token ({!set}, {!add}, {!move}, {!copy}) 344 - accept {!any} pointers, making them easy to use with {!of_string}. 345 Functions that require an existing element ({!remove}, {!replace}) 346 only accept {!nav} pointers. *) 347 ··· 350 351 For {!append} pointers, appends [value] to the end of the array. 352 353 - This accepts {!any} pointers directly from {!of_string}: 354 {[set (of_string "/tasks/-") json ~value:(Jsont.Json.string "new task")]} 355 356 - @raise Jsont.Error if the pointer doesn't resolve to an existing 357 location (except for {!append} pointers on arrays). *) 358 359 val add : any -> Jsont.json -> value:Jsont.json -> Jsont.json ··· 368 valid (0 to length inclusive).} 369 {- For {!append} pointers: Appends [value] to the array.}} 370 371 - @raise Jsont.Error if: 372 - The parent of the target location doesn't exist 373 - An array index is out of bounds (except for {!append} pointers) 374 - The parent is not an object or array *) ··· 379 For objects, removes the member. For arrays, removes the element 380 and shifts subsequent elements. 381 382 - @raise Jsont.Error if: 383 - [p] is the root (cannot remove the root) 384 - The pointer doesn't resolve to an existing value *) 385 ··· 388 389 Unlike {!add}, this requires the target to exist. 390 391 - @raise Jsont.Error if the pointer doesn't resolve to an existing value. *) 392 393 val move : from:nav t -> path:any -> Jsont.json -> Jsont.json 394 (** [move ~from ~path json] moves the value from [from] to [path]. ··· 396 This is equivalent to {!remove} at [from] followed by {!add} 397 at [path] with the removed value. 398 399 - @raise Jsont.Error if: 400 - [from] doesn't resolve to a value 401 - [path] is a proper prefix of [from] (would create a cycle) *) 402 ··· 406 This is equivalent to {!get} at [from] followed by {!add} 407 at [path] with the retrieved value. 408 409 - @raise Jsont.Error if [from] doesn't resolve to a value. *) 410 411 val test : nav t -> Jsont.json -> expected:Jsont.json -> bool 412 (** [test p json ~expected] tests if the value at [p] equals [expected]. ··· 511 The syntax is the same as RFC 6901 JSON Pointer, except [*] is allowed 512 as a reference token for array mapping. 513 514 - @raise Jsont.Error if [s] has invalid syntax. *) 515 516 val of_string_result : string -> (t, string) result 517 (** [of_string_result s] is like {!of_string} but returns a result. *) ··· 528 For [*] tokens on arrays, maps through all elements and collects results. 529 Results that are arrays are flattened into the output. 530 531 - @raise Jsont.Error if: 532 - A standard token doesn't resolve (member not found, index out of bounds) 533 - [*] is used on a non-array value 534 - [-] appears in the pointer (not supported in JMAP extended pointers) *) ··· 561 (Jsont.list Jsont.string) 562 ]} 563 564 - @raise Jsont.Error if the pointer fails to resolve (and no [absent]) 565 or if decoding with [codec] fails. *) 566 567 val path_list : t -> 'a Jsont.t -> 'a list Jsont.t ··· 576 Jmap.path (Jmap.of_string "/list/*/id") (Jsont.list Jsont.string) 577 ]} 578 579 - @raise Jsont.Error if pointer resolution fails, the result is not an array, 580 or any element fails to decode. *) 581 end
··· 77 (** [unescape s] unescapes a JSON Pointer reference token. 78 Specifically, [~1] becomes [/] and [~0] becomes [~]. 79 80 + @raise Jsont.Error.Error if [s] contains invalid escape sequences 81 (a [~] not followed by [0] or [1]). *) 82 end 83 ··· 137 138 (** {2 Existential wrapper} 139 140 + The {!type:any} type wraps a pointer of unknown phantom type, allowing 141 ergonomic use with mutation operations like {!set} and {!add} without 142 needing to pattern match on the pointer kind. *) 143 ··· 188 (** {2:coercion Coercion and inspection} *) 189 190 val any : _ t -> any 191 + (** [any p] wraps a typed pointer in the existential {!type:any} type. 192 + Use this when you have a [nav t] or [append t] but need an {!type:any} 193 for use with functions like {!set} or {!add}. *) 194 195 val is_nav : any -> bool ··· 202 203 val to_nav_exn : any -> nav t 204 (** [to_nav_exn p] returns the navigation pointer if [p] is one. 205 + @raise Jsont.Error.Error if [p] is an append pointer. *) 206 207 (** {2:parsing Parsing} *) 208 209 val of_string : string -> any 210 (** [of_string s] parses a JSON Pointer from its string representation. 211 212 + Returns an {!type:any} pointer that can be used directly with mutation 213 operations like {!set} and {!add}. For retrieval operations like 214 {!get}, use {!of_string_nav} instead. 215 ··· 217 with [/]. Each segment between [/] characters is unescaped as a 218 reference token. 219 220 + @raise Jsont.Error.Error if [s] has invalid syntax: 221 - Non-empty string not starting with [/] 222 - Invalid escape sequence ([~] not followed by [0] or [1]) 223 - [-] appears in non-final position *) ··· 230 differently, or when you need a typed pointer for operations that 231 require a specific kind. 232 233 + @raise Jsont.Error.Error if [s] has invalid syntax. *) 234 235 val of_string_nav : string -> nav t 236 (** [of_string_nav s] parses a JSON Pointer that must not contain [-]. ··· 238 Use this when you need a {!nav} pointer for retrieval operations 239 like {!get} or {!find}. 240 241 + @raise Jsont.Error.Error if [s] has invalid syntax or contains [-]. *) 242 243 val of_string_result : string -> (any, string) result 244 (** [of_string_result s] is like {!of_string} but returns a result ··· 251 according to {{:https://www.rfc-editor.org/rfc/rfc3986}RFC 3986}. 252 The leading [#] should {b not} be included in [s]. 253 254 + @raise Jsont.Error.Error on invalid syntax or invalid percent-encoding. *) 255 256 val of_uri_fragment_nav : string -> nav t 257 (** [of_uri_fragment_nav s] is like {!of_uri_fragment} but requires 258 the pointer to not contain [-]. 259 260 + @raise Jsont.Error.Error if invalid or contains [-]. *) 261 262 val of_uri_fragment_result : string -> (any, string) result 263 (** [of_uri_fragment_result s] is like {!of_uri_fragment} but returns ··· 310 311 (** {1 Evaluation} 312 313 + These functions evaluate a JSON Pointer against a {!type:Jsont.json} value 314 to retrieve the referenced value. They only accept {!nav} pointers 315 since {!append} pointers refer to nonexistent positions. *) 316 317 val get : nav t -> Jsont.json -> Jsont.json 318 (** [get p json] retrieves the value at pointer [p] in [json]. 319 320 + @raise Jsont.Error.Error if: 321 - The pointer references a nonexistent object member 322 - The pointer references an out-of-bounds array index 323 - An index type doesn't match the JSON value (e.g., [Nth] ··· 332 333 (** {1 Mutation} 334 335 + These functions modify a {!type:Jsont.json} value at a location specified 336 by a JSON Pointer. They are designed to support 337 {{:https://www.rfc-editor.org/rfc/rfc6902}RFC 6902 JSON Patch} 338 operations. ··· 341 applied; they do not mutate the input. 342 343 Functions that support the [-] token ({!set}, {!add}, {!move}, {!copy}) 344 + accept {!type:any} pointers, making them easy to use with {!of_string}. 345 Functions that require an existing element ({!remove}, {!replace}) 346 only accept {!nav} pointers. *) 347 ··· 350 351 For {!append} pointers, appends [value] to the end of the array. 352 353 + This accepts {!type:any} pointers directly from {!of_string}: 354 {[set (of_string "/tasks/-") json ~value:(Jsont.Json.string "new task")]} 355 356 + @raise Jsont.Error.Error if the pointer doesn't resolve to an existing 357 location (except for {!append} pointers on arrays). *) 358 359 val add : any -> Jsont.json -> value:Jsont.json -> Jsont.json ··· 368 valid (0 to length inclusive).} 369 {- For {!append} pointers: Appends [value] to the array.}} 370 371 + @raise Jsont.Error.Error if: 372 - The parent of the target location doesn't exist 373 - An array index is out of bounds (except for {!append} pointers) 374 - The parent is not an object or array *) ··· 379 For objects, removes the member. For arrays, removes the element 380 and shifts subsequent elements. 381 382 + @raise Jsont.Error.Error if: 383 - [p] is the root (cannot remove the root) 384 - The pointer doesn't resolve to an existing value *) 385 ··· 388 389 Unlike {!add}, this requires the target to exist. 390 391 + @raise Jsont.Error.Error if the pointer doesn't resolve to an existing value. *) 392 393 val move : from:nav t -> path:any -> Jsont.json -> Jsont.json 394 (** [move ~from ~path json] moves the value from [from] to [path]. ··· 396 This is equivalent to {!remove} at [from] followed by {!add} 397 at [path] with the removed value. 398 399 + @raise Jsont.Error.Error if: 400 - [from] doesn't resolve to a value 401 - [path] is a proper prefix of [from] (would create a cycle) *) 402 ··· 406 This is equivalent to {!get} at [from] followed by {!add} 407 at [path] with the retrieved value. 408 409 + @raise Jsont.Error.Error if [from] doesn't resolve to a value. *) 410 411 val test : nav t -> Jsont.json -> expected:Jsont.json -> bool 412 (** [test p json ~expected] tests if the value at [p] equals [expected]. ··· 511 The syntax is the same as RFC 6901 JSON Pointer, except [*] is allowed 512 as a reference token for array mapping. 513 514 + @raise Jsont.Error.Error if [s] has invalid syntax. *) 515 516 val of_string_result : string -> (t, string) result 517 (** [of_string_result s] is like {!of_string} but returns a result. *) ··· 528 For [*] tokens on arrays, maps through all elements and collects results. 529 Results that are arrays are flattened into the output. 530 531 + @raise Jsont.Error.Error if: 532 - A standard token doesn't resolve (member not found, index out of bounds) 533 - [*] is used on a non-array value 534 - [-] appears in the pointer (not supported in JMAP extended pointers) *) ··· 561 (Jsont.list Jsont.string) 562 ]} 563 564 + @raise Jsont.Error.Error if the pointer fails to resolve (and no [absent]) 565 or if decoding with [codec] fails. *) 566 567 val path_list : t -> 'a Jsont.t -> 'a list Jsont.t ··· 576 Jmap.path (Jmap.of_string "/list/*/id") (Jsont.list Jsont.string) 577 ]} 578 579 + @raise Jsont.Error.Error if pointer resolution fails, the result is not an array, 580 or any element fails to decode. *) 581 end
+4 -4
src/top/json_pointer_top.mli
··· 1 - (** Toplevel printers for {!Json_pointer}, {!Jsont.json}, and {!Jsont.Error.t}. 2 3 Printers are automatically installed when the library is loaded: 4 {[ ··· 12 ]} 13 14 JSON values will display as formatted JSON strings: 15 - {[ 16 # Jsont_bytesrw.decode_string Jsont.json {|{"foo": [1, 2]}|};; 17 - : Jsont.json = {"foo": [1, 2]} 18 - ]} 19 20 And errors will display as readable messages: 21 {[ ··· 32 Suitable for use with [#install_printer]. *) 33 34 val json_printer : Format.formatter -> Jsont.json -> unit 35 - (** [json_printer] formats a {!Jsont.json} value as a human-readable 36 JSON string. Suitable for use with [#install_printer]. *) 37 38 val error_printer : Format.formatter -> Jsont.Error.t -> unit
··· 1 + (** Toplevel printers for {!Json_pointer}, {!type:Jsont.json}, and {!Jsont.Error.t}. 2 3 Printers are automatically installed when the library is loaded: 4 {[ ··· 12 ]} 13 14 JSON values will display as formatted JSON strings: 15 + {v 16 # Jsont_bytesrw.decode_string Jsont.json {|{"foo": [1, 2]}|};; 17 - : Jsont.json = {"foo": [1, 2]} 18 + v} 19 20 And errors will display as readable messages: 21 {[ ··· 32 Suitable for use with [#install_printer]. *) 33 34 val json_printer : Format.formatter -> Jsont.json -> unit 35 + (** [json_printer] formats a {!type:Jsont.json} value as a human-readable 36 JSON string. Suitable for use with [#install_printer]. *) 37 38 val error_printer : Format.formatter -> Jsont.Error.t -> unit