···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 ;;
54get (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"];;
55to_string slash_ptr;;
56get 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 ;;
···62val find : nav t -> Jsont.json -> 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 *)
63type append (* A pointer ending with "-" (append position) *)
64type '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";;
69let 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"]}|};;
73add (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"}|};;
75remove (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");;
80test (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"];;
81to_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";;
83Token.escape "a/b";;
84Token.escape "a~b";;
85Token.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";;
···87to_uri_fragment (of_string_nav "/a~1b");;
88to_uri_fragment (of_string_nav "/c%d");;
89to_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> -> <code>#</code> -> whole document</li><li><code>"/foo"</code> -> <code>#/foo</code> -> <code>["bar", "baz"]</code></li><li><code>"/foo/0"</code> -> <code>#/foo/0</code> -> <code>"bar"</code></li><li><code>"/"</code> -> <code>#/</code> -> <code>0</code></li><li><code>"/a~1b"</code> -> <code>#/a~1b</code> -> <code>1</code></li><li><code>"/c%d"</code> -> <code>#/c%25d</code> -> <code>2</code></li><li><code>"/ "</code> -> <code>#/%20</code> -> <code>7</code></li><li><code>"/m~0n"</code> -> <code>#/m~0n</code> -> <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";;
92let creds_ptr = db_ptr / mem "credentials";;
93let user_ptr = creds_ptr / mem "username";;
94to_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";;
104let 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- |> 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"]}|};;
142set (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";;
144of_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 ;;
54get (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"];;
55to_string slash_ptr;;
56get 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 ;;
···62val find : nav t -> Jsont.json -> 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 *)
63type append (* A pointer ending with "-" (append position) *)
64type '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";;
69let 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"]}|};;
73add (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"}|};;
75remove (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");;
80test (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"];;
81to_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";;
83Token.escape "a/b";;
84Token.escape "a~b";;
85Token.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";;
···87to_uri_fragment (of_string_nav "/a~1b");;
88to_uri_fragment (of_string_nav "/c%d");;
89to_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> -> <code>#</code> -> whole document</li><li><code>"/foo"</code> -> <code>#/foo</code> -> <code>["bar", "baz"]</code></li><li><code>"/foo/0"</code> -> <code>#/foo/0</code> -> <code>"bar"</code></li><li><code>"/"</code> -> <code>#/</code> -> <code>0</code></li><li><code>"/a~1b"</code> -> <code>#/a~1b</code> -> <code>1</code></li><li><code>"/c%d"</code> -> <code>#/c%25d</code> -> <code>2</code></li><li><code>"/ "</code> -> <code>#/%20</code> -> <code>7</code></li><li><code>"/m~0n"</code> -> <code>#/m~0n</code> -> <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";;
92let creds_ptr = db_ptr / mem "credentials";;
93let user_ptr = creds_ptr / mem "username";;
94to_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";;
104let 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+ |> 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"]}|};;
142set (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";;
144of_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
···44val rfc_example : Jsont.json =
45 {"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}</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-{"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}</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 "/foo") rfc_example ;;
48- : Jsont.json = ["bar","baz"]</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 "/foo/0") rfc_example ;;
49- : Jsont.json = "bar"
50# get (of_string_nav "/foo/1") rfc_example ;;
···77- : nav t = [Mem "foo"; Mem "01"]</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 "-", 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 *)
78type append (* A pointer ending with "-" (append position) *)
79type '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 "/foo/0";;
81- : any = Any <abstr>
82# of_string "/foo/-";;
83-- : any = Any <abstr></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 "-" 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 {|{"foo":["a","b"]}|};;
84val arr_obj : Jsont.json = {"foo":["a","b"]}
85# add (of_string "/foo/-") arr_obj ~value:(Jsont.Json.string "c");;
86-- : Jsont.json = {"foo":["a","b","c"]}</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 "/foo/0";;
87- : nav t = [Mem "foo"; Nth 0]
88# of_string_nav "/foo/-";;
89Exception:
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 "/foo";;
91val nav_ptr : nav t = [Mem "foo"]
92# let app_ptr = at_end nav_ptr;;
93val app_ptr : append t = [Mem "foo"] /-
94# to_string app_ptr;;
95-- : string = "/foo/-"</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 {|{"foo":"bar"}|};;
96val obj : Jsont.json = {"foo":"bar"}
97# add (of_string "/baz") obj ~value:(Jsont.Json.string "qux");;
98-- : Jsont.json = {"foo":"bar","baz":"qux"}</code></pre><p>For arrays, <code>add</code> inserts BEFORE the specified index:</p><pre class="language-ocaml"><code># let arr_obj = parse_json {|{"foo":["a","b"]}|};;
99val arr_obj : Jsont.json = {"foo":["a","b"]}
100# add (of_string "/foo/1") arr_obj ~value:(Jsont.Json.string "X");;
101- : Jsont.json = {"foo":["a","X","b"]}</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 "/foo/-") arr_obj ~value:(Jsont.Json.string "c");;
102-- : Jsont.json = {"foo":["a","b","c"]}</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 "/foo"))) arr_obj ~value:(Jsont.Json.string "c");;
103-- : Jsont.json = {"foo":["a","b","c"]}</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 {|{"items":["x"]}|};;
104val items : Jsont.json = {"items":["x"]}
105# add (of_string "/items/0") items ~value:(Jsont.Json.string "y");;
106- : Jsont.json = {"items":["y","x"]}
107# add (of_string "/items/-") items ~value:(Jsont.Json.string "z");;
108-- : Jsont.json = {"items":["x","z"]}</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 {|{"foo":"bar","baz":"qux"}|};;
109val two_fields : Jsont.json = {"foo":"bar","baz":"qux"}
110# remove (of_string_nav "/baz") two_fields ;;
111- : Jsont.json = {"foo":"bar"}</code></pre><p>For arrays, it removes and shifts:</p><pre class="language-ocaml"><code># let three_elem = parse_json {|{"foo":["a","b","c"]}|};;
112val three_elem : Jsont.json = {"foo":["a","b","c"]}
113# remove (of_string_nav "/foo/1") three_elem ;;
114-- : Jsont.json = {"foo":["a","c"]}</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 "/foo") obj ~value:(Jsont.Json.string "baz")
115 ;;
116-- : Jsont.json = {"foo":"baz"}</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 {|{"foo":{"bar":"baz"},"qux":{}}|};;
117val nested : Jsont.json = {"foo":{"bar":"baz"},"qux":{}}
118# move ~from:(of_string_nav "/foo/bar") ~path:(of_string "/qux/thud") nested;;
119-- : Jsont.json = {"foo":{},"qux":{"thud":"baz"}}</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 {|{"foo":{"bar":"baz"}}|};;
120val to_copy : Jsont.json = {"foo":{"bar":"baz"}}
121# copy ~from:(of_string_nav "/foo/bar") ~path:(of_string "/foo/qux") to_copy;;
122-- : Jsont.json = {"foo":{"bar":"baz","qux":"baz"}}</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 "/foo") obj ~expected:(Jsont.Json.string "bar");;
123- : bool = true
124# test (of_string_nav "/foo") obj ~expected:(Jsont.Json.string "wrong");;
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 "a/b"];;
···127# to_string p;;
128- : string = "/a~1b"
129# of_string_nav "/a~1b";;
130-- : nav t = [Mem "a/b"]</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 "hello";;
131- : string = "hello"
132# Token.escape "a/b";;
133- : string = "a~1b"
···148- : string = "/%20"</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>""</code> -> <code>#</code> -> whole document</li><li><code>"/foo"</code> -> <code>#/foo</code> -> <code>["bar", "baz"]</code></li><li><code>"/foo/0"</code> -> <code>#/foo/0</code> -> <code>"bar"</code></li><li><code>"/"</code> -> <code>#/</code> -> <code>0</code></li><li><code>"/a~1b"</code> -> <code>#/a~1b</code> -> <code>1</code></li><li><code>"/c%d"</code> -> <code>#/c%25d</code> -> <code>2</code></li><li><code>"/ "</code> -> <code>#/%20</code> -> <code>7</code></li><li><code>"/m~0n"</code> -> <code>#/m~0n</code> -> <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 "database"; mem "port"];;
149val port_ptr : nav t = [Mem "database"; Mem "port"]
150# to_string port_ptr;;
151-- : string = "/database/port"</code></pre><p>For array access, use the <code>nth</code> helper:</p><pre class="language-ocaml"><code># let first_feature_ptr = make [mem "features"; nth 0];;
152val first_feature_ptr : nav t = [Mem "features"; Nth 0]
153# to_string first_feature_ptr;;
154-- : string = "/features/0"</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 "/database";;
155val db_ptr : nav t = [Mem "database"]
156# let creds_ptr = db_ptr / mem "credentials";;
157val creds_ptr : nav t = [Mem "database"; Mem "credentials"]
···172 "features": ["auth", "logging", "metrics"]
173 }|};;
174val config_json : Jsont.json =
175- {"database":{"host":"localhost","port":5432,"credentials":{"username":"admin","password":"secret"}},"features":["auth","logging","metrics"]}</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 "/database/host";;
176val nav : nav t = [Mem "database"; Mem "host"]
177# let db_host =
178 Jsont.Json.decode
···221 (path (of_string_nav "/database/port") Jsont.int)
222 config_json
223 |> 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 {|{"tasks":["buy milk"]}|};;
225val tasks : Jsont.json = {"tasks":["buy milk"]}
226# set (of_string "/tasks/0") tasks ~value:(Jsont.Json.string "buy eggs");;
227- : Jsont.json = {"tasks":["buy eggs"]}
228# set (of_string "/tasks/-") tasks ~value:(Jsont.Json.string "call mom");;
229-- : Jsont.json = {"tasks":["buy milk","call mom"]}</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>"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><pre class="language-ocaml"><code># of_string_kind "/tasks/0";;
230- : [ `Append of append t | `Nav of nav t ] = `Nav [Mem "tasks"; Nth 0]
231# of_string_kind "/tasks/-";;
232- : [ `Append of append t | `Nav of nav t ] = `Append [Mem "tasks"] /-</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 "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></body></html>
···44val rfc_example : Jsont.json =
45 {"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}</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+{"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}</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 "/foo") rfc_example ;;
48- : Jsont.json = ["bar","baz"]</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 "/foo/0") rfc_example ;;
49- : Jsont.json = "bar"
50# get (of_string_nav "/foo/1") rfc_example ;;
···77- : nav t = [Mem "foo"; Mem "01"]</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 "-", 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 *)
78type append (* A pointer ending with "-" (append position) *)
79type '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 "/foo/0";;
81- : any = Any <abstr>
82# of_string "/foo/-";;
83+- : any = Any <abstr></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 "-" 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 {|{"foo":["a","b"]}|};;
84val arr_obj : Jsont.json = {"foo":["a","b"]}
85# add (of_string "/foo/-") arr_obj ~value:(Jsont.Json.string "c");;
86+- : Jsont.json = {"foo":["a","b","c"]}</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 "/foo/0";;
87- : nav t = [Mem "foo"; Nth 0]
88# of_string_nav "/foo/-";;
89Exception:
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 "/foo";;
91val nav_ptr : nav t = [Mem "foo"]
92# let app_ptr = at_end nav_ptr;;
93val app_ptr : append t = [Mem "foo"] /-
94# to_string app_ptr;;
95+- : string = "/foo/-"</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 {|{"foo":"bar"}|};;
96val obj : Jsont.json = {"foo":"bar"}
97# add (of_string "/baz") obj ~value:(Jsont.Json.string "qux");;
98+- : Jsont.json = {"foo":"bar","baz":"qux"}</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 {|{"foo":["a","b"]}|};;
99val arr_obj : Jsont.json = {"foo":["a","b"]}
100# add (of_string "/foo/1") arr_obj ~value:(Jsont.Json.string "X");;
101- : Jsont.json = {"foo":["a","X","b"]}</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 "/foo/-") arr_obj ~value:(Jsont.Json.string "c");;
102+- : Jsont.json = {"foo":["a","b","c"]}</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 "/foo"))) arr_obj ~value:(Jsont.Json.string "c");;
103+- : Jsont.json = {"foo":["a","b","c"]}</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 {|{"items":["x"]}|};;
104val items : Jsont.json = {"items":["x"]}
105# add (of_string "/items/0") items ~value:(Jsont.Json.string "y");;
106- : Jsont.json = {"items":["y","x"]}
107# add (of_string "/items/-") items ~value:(Jsont.Json.string "z");;
108+- : Jsont.json = {"items":["x","z"]}</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 {|{"foo":"bar","baz":"qux"}|};;
109val two_fields : Jsont.json = {"foo":"bar","baz":"qux"}
110# remove (of_string_nav "/baz") two_fields ;;
111- : Jsont.json = {"foo":"bar"}</code></pre><p>For arrays, it removes and shifts:</p><pre class="language-ocaml"><code># let three_elem = parse_json {|{"foo":["a","b","c"]}|};;
112val three_elem : Jsont.json = {"foo":["a","b","c"]}
113# remove (of_string_nav "/foo/1") three_elem ;;
114+- : Jsont.json = {"foo":["a","c"]}</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 "/foo") obj ~value:(Jsont.Json.string "baz")
115 ;;
116+- : Jsont.json = {"foo":"baz"}</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 {|{"foo":{"bar":"baz"},"qux":{}}|};;
117val nested : Jsont.json = {"foo":{"bar":"baz"},"qux":{}}
118# move ~from:(of_string_nav "/foo/bar") ~path:(of_string "/qux/thud") nested;;
119+- : Jsont.json = {"foo":{},"qux":{"thud":"baz"}}</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 {|{"foo":{"bar":"baz"}}|};;
120val to_copy : Jsont.json = {"foo":{"bar":"baz"}}
121# copy ~from:(of_string_nav "/foo/bar") ~path:(of_string "/foo/qux") to_copy;;
122+- : Jsont.json = {"foo":{"bar":"baz","qux":"baz"}}</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 "/foo") obj ~expected:(Jsont.Json.string "bar");;
123- : bool = true
124# test (of_string_nav "/foo") obj ~expected:(Jsont.Json.string "wrong");;
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 "a/b"];;
···127# to_string p;;
128- : string = "/a~1b"
129# of_string_nav "/a~1b";;
130+- : nav t = [Mem "a/b"]</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 "hello";;
131- : string = "hello"
132# Token.escape "a/b";;
133- : string = "a~1b"
···148- : string = "/%20"</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>""</code> -> <code>#</code> -> whole document</li><li><code>"/foo"</code> -> <code>#/foo</code> -> <code>["bar", "baz"]</code></li><li><code>"/foo/0"</code> -> <code>#/foo/0</code> -> <code>"bar"</code></li><li><code>"/"</code> -> <code>#/</code> -> <code>0</code></li><li><code>"/a~1b"</code> -> <code>#/a~1b</code> -> <code>1</code></li><li><code>"/c%d"</code> -> <code>#/c%25d</code> -> <code>2</code></li><li><code>"/ "</code> -> <code>#/%20</code> -> <code>7</code></li><li><code>"/m~0n"</code> -> <code>#/m~0n</code> -> <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 "database"; mem "port"];;
149val port_ptr : nav t = [Mem "database"; Mem "port"]
150# to_string port_ptr;;
151+- : string = "/database/port"</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 "features"; nth 0];;
152val first_feature_ptr : nav t = [Mem "features"; Nth 0]
153# to_string first_feature_ptr;;
154+- : string = "/features/0"</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 "/database";;
155val db_ptr : nav t = [Mem "database"]
156# let creds_ptr = db_ptr / mem "credentials";;
157val creds_ptr : nav t = [Mem "database"; Mem "credentials"]
···172 "features": ["auth", "logging", "metrics"]
173 }|};;
174val config_json : Jsont.json =
175+ {"database":{"host":"localhost","port":5432,"credentials":{"username":"admin","password":"secret"}},"features":["auth","logging","metrics"]}</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 "/database/host";;
176val nav : nav t = [Mem "database"; Mem "host"]
177# let db_host =
178 Jsont.Json.decode
···221 (path (of_string_nav "/database/port") Jsont.int)
222 config_json
223 |> 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 {|{"tasks":["buy milk"]}|};;
225val tasks : Jsont.json = {"tasks":["buy milk"]}
226# set (of_string "/tasks/0") tasks ~value:(Jsont.Json.string "buy eggs");;
227- : Jsont.json = {"tasks":["buy eggs"]}
228# set (of_string "/tasks/-") tasks ~value:(Jsont.Json.string "call mom");;
229+- : Jsont.json = {"tasks":["buy milk","call mom"]}</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>"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><pre class="language-ocaml"><code># of_string_kind "/tasks/0";;
230- : [ `Append of append t | `Nav of nav t ] = `Nav [Mem "tasks"; Nth 0]
231# of_string_kind "/tasks/-";;
232- : [ `Append of append t | `Nav of nav t ] = `Append [Mem "tasks"] /-</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 "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></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]}
207208-The empty pointer ({!root}) returns the whole document.
209210{2 Object Member Access}
211···376type any (* Existential: wraps either nav or append *)
377v}
378379-When you parse a pointer with {!of_string}, you get an {!any} pointer
380that can be used directly with mutation operations:
381382{@ocaml[
···386- : any = Any <abstr>
387]}
388389-The [-] creates an append pointer. The {!any} type wraps either kind,
390-making it ergonomic to use with operations like {!set} and {!add}.
391392{2 Why Two Pointer Types?}
393···400So you {b cannot use [get] or [find]} with an append pointer - it makes
401no sense to retrieve a value from a position that doesn't exist! The
402library enforces this:
403-- Use {!of_string_nav} when you need to call {!get} or {!find}
404-- Use {!of_string} (returns {!any}) for mutation operations
405406-Mutation operations like {!add} accept {!any} directly:
407408{x@ocaml[
409# let arr_obj = parse_json {|{"foo":["a","b"]}|};;
···412- : Jsont.json = {"foo":["a","b","c"]}
413]x}
414415-For retrieval operations, use {!of_string_nav} which ensures the pointer
416doesn't contain [-]:
417418{@ocaml[
···425426{2 Creating Append Pointers Programmatically}
427428-You can convert a navigation pointer to an append pointer using {!at_end}:
429430{@ocaml[
431# let nav_ptr = of_string_nav "/foo";;
···444445{2 Add}
446447-The {!add} operation inserts a value at a location. It accepts {!any}
448-pointers, so you can use {!of_string} directly:
449450{x@ocaml[
451# let obj = parse_json {|{"foo":"bar"}|};;
···454- : Jsont.json = {"foo":"bar","baz":"qux"}
455]x}
456457-For arrays, {!add} inserts BEFORE the specified index:
458459{x@ocaml[
460# let arr_obj = parse_json {|{"foo":["a","b"]}|};;
···470- : Jsont.json = {"foo":["a","b","c"]}
471]x}
472473-You can also use {!at_end} to create an append pointer programmatically:
474475{x@ocaml[
476# add (any (at_end (of_string_nav "/foo"))) arr_obj ~value:(Jsont.Json.string "c");;
···479480{2 Ergonomic Mutation with [any]}
481482-Since {!add}, {!set}, {!move}, and {!copy} accept {!any} pointers, you can
483-use {!of_string} directly without any pattern matching. This makes JSON
484Patch implementations straightforward:
485486{x@ocaml[
···497498{2 Remove}
499500-The {!remove} operation deletes a value. It only accepts [nav t] because
501you can only remove something that exists:
502503{x@ocaml[
···518519{2 Replace}
520521-The {!replace} operation updates an existing value:
522523{@ocaml[
524# replace (of_string_nav "/foo") obj ~value:(Jsont.Json.string "baz")
···526- : Jsont.json = {"foo":"baz"}
527]}
528529-Unlike {!add}, {!replace} requires the target to already exist (hence [nav t]).
530Attempting to replace a nonexistent path raises an error.
531532{2 Move}
533534-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}:
537538{x@ocaml[
539# let nested = parse_json {|{"foo":{"bar":"baz"},"qux":{}}|};;
···544545{2 Copy}
546547-The {!copy} operation duplicates a value (same typing as {!move}):
548549{x@ocaml[
550# let to_copy = parse_json {|{"foo":{"bar":"baz"}}|};;
···555556{2 Test}
557558-The {!test} operation verifies a value (useful in JSON Patch):
559560{@ocaml[
561# test (of_string_nav "/foo") obj ~expected:(Jsont.Json.string "bar");;
···597598{2 Escaping in Action}
599600-The {!Token} module exposes the escaping functions:
601602{@ocaml[
603# Token.escape "hello";;
···692- : string = "/database/port"
693]}
694695-For array access, use the {!nth} helper:
696697{@ocaml[
698# let first_feature_ptr = make [mem "features"; nth 0];;
···703704{2 Pointer Navigation}
705706-You can build pointers incrementally using the [/] operator (or {!append_index}):
707708{@ocaml[
709# let db_ptr = of_string_nav "/database";;
···749750{2 Typed Access with [path]}
751752-The {!path} combinator combines pointer navigation with typed decoding:
753754{@ocaml[
755# let nav = of_string_nav "/database/host";;
···844845{2 Updates with Polymorphic Pointers}
846847-The {!set} and {!add} functions accept {!any} pointers, which means you can
848-use the result of {!of_string} directly without pattern matching:
849850{x@ocaml[
851# let tasks = parse_json {|{"tasks":["buy milk"]}|};;
···859This is useful for implementing JSON Patch ({{:https://datatracker.ietf.org/doc/html/rfc6902}RFC 6902}) where
860operations like ["add"] can target either existing positions or the
861append marker. If you need to distinguish between pointer types at runtime,
862-use {!of_string_kind} which returns a polymorphic variant:
863864{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]}
207208+The empty pointer ({!Json_pointer.root}) returns the whole document.
209210{2 Object Member Access}
211···376type any (* Existential: wraps either nav or append *)
377v}
378379+When you parse a pointer with {!Json_pointer.of_string}, you get an {!type:Json_pointer.any} pointer
380that can be used directly with mutation operations:
381382{@ocaml[
···386- : any = Any <abstr>
387]}
388389+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}.
391392{2 Why Two Pointer Types?}
393···400So you {b cannot use [get] or [find]} with an append pointer - it makes
401no sense to retrieve a value from a position that doesn't exist! The
402library enforces this:
403+- Use {!Json_pointer.of_string_nav} when you need to call {!Json_pointer.get} or {!Json_pointer.find}
404+- Use {!Json_pointer.of_string} (returns {!type:Json_pointer.any}) for mutation operations
405406+Mutation operations like {!Json_pointer.add} accept {!type:Json_pointer.any} directly:
407408{x@ocaml[
409# let arr_obj = parse_json {|{"foo":["a","b"]}|};;
···412- : Jsont.json = {"foo":["a","b","c"]}
413]x}
414415+For retrieval operations, use {!Json_pointer.of_string_nav} which ensures the pointer
416doesn't contain [-]:
417418{@ocaml[
···425426{2 Creating Append Pointers Programmatically}
427428+You can convert a navigation pointer to an append pointer using {!Json_pointer.at_end}:
429430{@ocaml[
431# let nav_ptr = of_string_nav "/foo";;
···444445{2 Add}
446447+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:
449450{x@ocaml[
451# let obj = parse_json {|{"foo":"bar"}|};;
···454- : Jsont.json = {"foo":"bar","baz":"qux"}
455]x}
456457+For arrays, {!Json_pointer.add} inserts BEFORE the specified index:
458459{x@ocaml[
460# let arr_obj = parse_json {|{"foo":["a","b"]}|};;
···470- : Jsont.json = {"foo":["a","b","c"]}
471]x}
472473+You can also use {!Json_pointer.at_end} to create an append pointer programmatically:
474475{x@ocaml[
476# add (any (at_end (of_string_nav "/foo"))) arr_obj ~value:(Jsont.Json.string "c");;
···479480{2 Ergonomic Mutation with [any]}
481482+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
484Patch implementations straightforward:
485486{x@ocaml[
···497498{2 Remove}
499500+The {!Json_pointer.remove} operation deletes a value. It only accepts [nav t] because
501you can only remove something that exists:
502503{x@ocaml[
···518519{2 Replace}
520521+The {!Json_pointer.replace} operation updates an existing value:
522523{@ocaml[
524# replace (of_string_nav "/foo") obj ~value:(Jsont.Json.string "baz")
···526- : Jsont.json = {"foo":"baz"}
527]}
528529+Unlike {!Json_pointer.add}, {!Json_pointer.replace} requires the target to already exist (hence [nav t]).
530Attempting to replace a nonexistent path raises an error.
531532{2 Move}
533534+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}:
537538{x@ocaml[
539# let nested = parse_json {|{"foo":{"bar":"baz"},"qux":{}}|};;
···544545{2 Copy}
546547+The {!Json_pointer.copy} operation duplicates a value (same typing as {!Json_pointer.move}):
548549{x@ocaml[
550# let to_copy = parse_json {|{"foo":{"bar":"baz"}}|};;
···555556{2 Test}
557558+The {!Json_pointer.test} operation verifies a value (useful in JSON Patch):
559560{@ocaml[
561# test (of_string_nav "/foo") obj ~expected:(Jsont.Json.string "bar");;
···597598{2 Escaping in Action}
599600+The {!Json_pointer.Token} module exposes the escaping functions:
601602{@ocaml[
603# Token.escape "hello";;
···692- : string = "/database/port"
693]}
694695+For array access, use the {!Json_pointer.nth} helper:
696697{@ocaml[
698# let first_feature_ptr = make [mem "features"; nth 0];;
···703704{2 Pointer Navigation}
705706+You can build pointers incrementally using the [/] operator (or {!Json_pointer.append_index}):
707708{@ocaml[
709# let db_ptr = of_string_nav "/database";;
···749750{2 Typed Access with [path]}
751752+The {!Json_pointer.path} combinator combines pointer navigation with typed decoding:
753754{@ocaml[
755# let nav = of_string_nav "/database/host";;
···844845{2 Updates with Polymorphic Pointers}
846847+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:
849850{x@ocaml[
851# let tasks = parse_json {|{"tasks":["buy milk"]}|};;
···859This is useful for implementing JSON Patch ({{:https://datatracker.ietf.org/doc/html/rfc6902}RFC 6902}) where
860operations like ["add"] can target either existing positions or the
861append marker. If you need to distinguish between pointer types at runtime,
862+use {!Json_pointer.of_string_kind} which returns a polymorphic variant:
863864{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 [~].
7980- @raise Jsont.Error if [s] contains invalid escape sequences
81 (a [~] not followed by [0] or [1]). *)
82end
83···137138(** {2 Existential wrapper}
139140- 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} *)
189190val 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}. *)
194195val is_nav : any -> bool
···202203val 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. *)
206207(** {2:parsing Parsing} *)
208209val of_string : string -> any
210(** [of_string s] parses a JSON Pointer from its string representation.
211212- 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.
219220- @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.
232233- @raise Jsont.Error if [s] has invalid syntax. *)
234235val 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}.
240241- @raise Jsont.Error if [s] has invalid syntax or contains [-]. *)
242243val 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].
253254- @raise Jsont.Error on invalid syntax or invalid percent-encoding. *)
255256val 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 [-].
259260- @raise Jsont.Error if invalid or contains [-]. *)
261262val of_uri_fragment_result : string -> (any, string) result
263(** [of_uri_fragment_result s] is like {!of_uri_fragment} but returns
···310311(** {1 Evaluation}
312313- 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. *)
316317val get : nav t -> Jsont.json -> Jsont.json
318(** [get p json] retrieves the value at pointer [p] in [json].
319320- @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]
···332333(** {1 Mutation}
334335- 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.
342343 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···350351 For {!append} pointers, appends [value] to the end of the array.
352353- This accepts {!any} pointers directly from {!of_string}:
354 {[set (of_string "/tasks/-") json ~value:(Jsont.Json.string "new task")]}
355356- @raise Jsont.Error if the pointer doesn't resolve to an existing
357 location (except for {!append} pointers on arrays). *)
358359val add : any -> Jsont.json -> value:Jsont.json -> Jsont.json
···368 valid (0 to length inclusive).}
369 {- For {!append} pointers: Appends [value] to the array.}}
370371- @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.
381382- @raise Jsont.Error if:
383 - [p] is the root (cannot remove the root)
384 - The pointer doesn't resolve to an existing value *)
385···388389 Unlike {!add}, this requires the target to exist.
390391- @raise Jsont.Error if the pointer doesn't resolve to an existing value. *)
392393val 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.
398399- @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.
408409- @raise Jsont.Error if [from] doesn't resolve to a value. *)
410411val 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.
513514- @raise Jsont.Error if [s] has invalid syntax. *)
515516 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.
530531- @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 ]}
563564- @raise Jsont.Error if the pointer fails to resolve (and no [absent])
565 or if decoding with [codec] fails. *)
566567 val path_list : t -> 'a Jsont.t -> 'a list Jsont.t
···576 Jmap.path (Jmap.of_string "/list/*/id") (Jsont.list Jsont.string)
577 ]}
578579- @raise Jsont.Error if pointer resolution fails, the result is not an array,
580 or any element fails to decode. *)
581end
···77 (** [unescape s] unescapes a JSON Pointer reference token.
78 Specifically, [~1] becomes [/] and [~0] becomes [~].
7980+ @raise Jsont.Error.Error if [s] contains invalid escape sequences
81 (a [~] not followed by [0] or [1]). *)
82end
83···137138(** {2 Existential wrapper}
139140+ 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} *)
189190val 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}. *)
194195val is_nav : any -> bool
···202203val 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. *)
206207(** {2:parsing Parsing} *)
208209val of_string : string -> any
210(** [of_string s] parses a JSON Pointer from its string representation.
211212+ 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.
219220+ @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.
232233+ @raise Jsont.Error.Error if [s] has invalid syntax. *)
234235val 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}.
240241+ @raise Jsont.Error.Error if [s] has invalid syntax or contains [-]. *)
242243val 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].
253254+ @raise Jsont.Error.Error on invalid syntax or invalid percent-encoding. *)
255256val 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 [-].
259260+ @raise Jsont.Error.Error if invalid or contains [-]. *)
261262val of_uri_fragment_result : string -> (any, string) result
263(** [of_uri_fragment_result s] is like {!of_uri_fragment} but returns
···310311(** {1 Evaluation}
312313+ 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. *)
316317val get : nav t -> Jsont.json -> Jsont.json
318(** [get p json] retrieves the value at pointer [p] in [json].
319320+ @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]
···332333(** {1 Mutation}
334335+ 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.
342343 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···350351 For {!append} pointers, appends [value] to the end of the array.
352353+ This accepts {!type:any} pointers directly from {!of_string}:
354 {[set (of_string "/tasks/-") json ~value:(Jsont.Json.string "new task")]}
355356+ @raise Jsont.Error.Error if the pointer doesn't resolve to an existing
357 location (except for {!append} pointers on arrays). *)
358359val add : any -> Jsont.json -> value:Jsont.json -> Jsont.json
···368 valid (0 to length inclusive).}
369 {- For {!append} pointers: Appends [value] to the array.}}
370371+ @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.
381382+ @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···388389 Unlike {!add}, this requires the target to exist.
390391+ @raise Jsont.Error.Error if the pointer doesn't resolve to an existing value. *)
392393val 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.
398399+ @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.
408409+ @raise Jsont.Error.Error if [from] doesn't resolve to a value. *)
410411val 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.
513514+ @raise Jsont.Error.Error if [s] has invalid syntax. *)
515516 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.
530531+ @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 ]}
563564+ @raise Jsont.Error.Error if the pointer fails to resolve (and no [absent])
565 or if decoding with [codec] fails. *)
566567 val path_list : t -> 'a Jsont.t -> 'a list Jsont.t
···576 Jmap.path (Jmap.of_string "/list/*/id") (Jsont.list Jsont.string)
577 ]}
578579+ @raise Jsont.Error.Error if pointer resolution fails, the result is not an array,
580 or any element fails to decode. *)
581end
+4-4
src/top/json_pointer_top.mli
···1-(** Toplevel printers for {!Json_pointer}, {!Jsont.json}, and {!Jsont.Error.t}.
23 Printers are automatically installed when the library is loaded:
4 {[
···12 ]}
1314 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- ]}
1920 And errors will display as readable messages:
21 {[
···32 Suitable for use with [#install_printer]. *)
3334val 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]. *)
3738val error_printer : Format.formatter -> Jsont.Error.t -> unit
···1+(** Toplevel printers for {!Json_pointer}, {!type:Jsont.json}, and {!Jsont.Error.t}.
23 Printers are automatically installed when the library is loaded:
4 {[
···12 ]}
1314 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}
1920 And errors will display as readable messages:
21 {[
···32 Suitable for use with [#install_printer]. *)
3334val 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]. *)
3738val error_printer : Format.formatter -> Jsont.Error.t -> unit