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