{0 JSON Pointer Tutorial} This tutorial introduces JSON Pointer as defined in {{:https://www.rfc-editor.org/rfc/rfc6901} RFC 6901}, and demonstrates the [json-pointer] OCaml library through interactive examples. {1 JSON Pointer vs JSON Path} Before diving in, it's worth understanding the difference between JSON Pointer and JSON Path, as they serve different purposes: {b JSON Pointer} ({{:https://datatracker.ietf.org/doc/html/rfc6901}RFC 6901}) is an {e indicator syntax} that specifies a {e single location} within JSON data. It always identifies at most one value. {b JSON Path} is a {e query syntax} that can {e search} JSON data and return {e multiple} values matching specified criteria. Use JSON Pointer when you need to address a single, specific location (like JSON Schema's [$ref]). Use JSON Path when you might need multiple results (like Kubernetes queries). The [json-pointer] library implements JSON Pointer and integrates with the {!Jsont.Path} type for representing navigation indices. {1 Setup} First, let's set up our environment. In the toplevel, you can load the library with [#require "json-pointer.top";;] which will automatically install pretty printers. {@ocaml[ # Json_pointer_top.install ();; - : unit = () # open Json_pointer;; # let parse_json s = match Jsont_bytesrw.decode_string Jsont.json s with | Ok json -> json | Error e -> failwith e;; val parse_json : string -> Jsont.json = ]} {1 What is JSON Pointer?} From {{:https://datatracker.ietf.org/doc/html/rfc6901#section-1}RFC 6901, Section 1}: {i JSON Pointer defines a string syntax for identifying a specific value within a JavaScript Object Notation (JSON) document.} In other words, JSON Pointer is an addressing scheme for locating values inside a JSON structure. Think of it like a filesystem path, but for JSON documents instead of files. For example, given this JSON document: {x@ocaml[ # let users_json = parse_json {|{ "users": [ {"name": "Alice", "age": 30}, {"name": "Bob", "age": 25} ] }|};; val users_json : Jsont.json = {"users":[{"name":"Alice","age":30},{"name":"Bob","age":25}]} ]x} The JSON Pointer [/users/0/name] refers to the string ["Alice"]: {@ocaml[ # let ptr = of_string_nav "/users/0/name";; val ptr : nav t = [Mem "users"; Nth 0; Mem "name"] # get ptr users_json;; - : Jsont.json = "Alice" ]} In OCaml, this is represented by the ['a Json_pointer.t] type - a sequence of navigation steps from the document root to a target value. The phantom type parameter ['a] encodes whether this is a navigation pointer or an append pointer (more on this later). {1 Syntax: Reference Tokens} {{:https://datatracker.ietf.org/doc/html/rfc6901#section-3}RFC 6901, Section 3} defines the syntax: {i A JSON Pointer is a Unicode string containing a sequence of zero or more reference tokens, each prefixed by a '/' (%x2F) character.} The grammar is elegantly simple: {v json-pointer = *( "/" reference-token ) reference-token = *( unescaped / escaped ) v} This means: - The empty string [""] is a valid pointer (it refers to the whole document) - Every non-empty pointer starts with [/] - Everything between [/] characters is a "reference token" Let's see this in action: {@ocaml[ # of_string_nav "";; - : nav t = [] ]} The empty pointer has no reference tokens - it points to the root. {@ocaml[ # of_string_nav "/foo";; - : nav t = [Mem "foo"] ]} The pointer [/foo] has one token: [foo]. Since it's not a number, it's interpreted as an object member name ([Mem]). {@ocaml[ # of_string_nav "/foo/0";; - : nav t = [Mem "foo"; Nth 0] ]} Here we have two tokens: [foo] (a member name) and [0] (interpreted as an array index [Nth]). {@ocaml[ # of_string_nav "/foo/bar/baz";; - : nav t = [Mem "foo"; Mem "bar"; Mem "baz"] ]} Multiple tokens navigate deeper into nested structures. {2 The Index Type} Each reference token is represented using {!Jsont.Path.index}: {v type index = Jsont.Path.index (* = Jsont.Path.Mem of string * Jsont.Meta.t | Jsont.Path.Nth of int * Jsont.Meta.t *) v} The [Mem] constructor is for object member access, and [Nth] is for array index access. The member name is {b unescaped} - you work with the actual key string (like ["a/b"]) and the library handles any escaping needed for the JSON Pointer string representation. {2 Invalid Syntax} What happens if a pointer doesn't start with [/]? {@ocaml[ # of_string_nav "foo";; Exception: Jsont.Error Invalid JSON Pointer: must be empty or start with '/': foo. ]} The RFC is strict: non-empty pointers MUST start with [/]. For safer parsing, use [of_string_result]: {@ocaml[ # of_string_result "foo";; - : (any, string) result = Error "Invalid JSON Pointer: must be empty or start with '/': foo" # of_string_result "/valid";; - : (any, string) result = Ok (Any ) ]} {1 Evaluation: Navigating JSON} Now we come to the heart of JSON Pointer: evaluation. {{:https://datatracker.ietf.org/doc/html/rfc6901#section-4}RFC 6901, Section 4} describes how a pointer is resolved against a JSON document: {i Evaluation of a JSON Pointer begins with a reference to the root value of a JSON document and completes with a reference to some value within the document. Each reference token in the JSON Pointer is evaluated sequentially.} Let's use the example JSON document from {{:https://datatracker.ietf.org/doc/html/rfc6901#section-5}RFC 6901, Section 5}: {x@ocaml[ # let rfc_example = parse_json {|{ "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 }|};; val rfc_example : Jsont.json = {"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} ]x} This document is carefully constructed to exercise various edge cases! {2 The Root Pointer} {@ocaml[ # get root rfc_example ;; - : Jsont.json = {"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} ]} The empty pointer ({!Json_pointer.root}) returns the whole document. {2 Object Member Access} {@ocaml[ # get (of_string_nav "/foo") rfc_example ;; - : Jsont.json = ["bar","baz"] ]} [/foo] accesses the member named [foo], which is an array. {2 Array Index Access} {@ocaml[ # get (of_string_nav "/foo/0") rfc_example ;; - : Jsont.json = "bar" # get (of_string_nav "/foo/1") rfc_example ;; - : Jsont.json = "baz" ]} [/foo/0] first goes to [foo], then accesses index 0 of the array. {2 Empty String as Key} JSON allows empty strings as object keys: {@ocaml[ # get (of_string_nav "/") rfc_example ;; - : Jsont.json = 0 ]} The pointer [/] has one token: the empty string. This accesses the member with an empty name. {2 Keys with Special Characters} The RFC example includes keys with [/] and [~] characters: {@ocaml[ # get (of_string_nav "/a~1b") rfc_example ;; - : Jsont.json = 1 ]} The token [a~1b] refers to the key [a/b]. We'll explain this escaping {{:#escaping}below}. {@ocaml[ # get (of_string_nav "/m~0n") rfc_example ;; - : Jsont.json = 8 ]} The token [m~0n] refers to the key [m~n]. {b Important}: When using the OCaml library programmatically, you don't need to worry about escaping. The [Mem] variant holds the literal key name: {@ocaml[ # let slash_ptr = make [mem "a/b"];; val slash_ptr : nav t = [Mem "a/b"] # to_string slash_ptr;; - : string = "/a~1b" # get slash_ptr rfc_example ;; - : Jsont.json = 1 ]} The library escapes it when converting to string. {2 Other Special Characters (No Escaping Needed)} Most characters don't need escaping in JSON Pointer strings: {@ocaml[ # get (of_string_nav "/c%d") rfc_example ;; - : Jsont.json = 2 # get (of_string_nav "/e^f") rfc_example ;; - : Jsont.json = 3 # get (of_string_nav "/g|h") rfc_example ;; - : Jsont.json = 4 # get (of_string_nav "/ ") rfc_example ;; - : Jsont.json = 7 ]} Even a space is a valid key character! {2 Error Conditions} What happens when we try to access something that doesn't exist? {@ocaml[ # get_result (of_string_nav "/nonexistent") rfc_example;; - : (Jsont.json, Jsont.Error.t) result = Error JSON Pointer: member 'nonexistent' not found File "-": # find (of_string_nav "/nonexistent") rfc_example;; - : Jsont.json option = None ]} Or an out-of-bounds array index: {@ocaml[ # find (of_string_nav "/foo/99") rfc_example;; - : Jsont.json option = None ]} Or try to index into a non-container: {@ocaml[ # find (of_string_nav "/foo/0/invalid") rfc_example;; - : Jsont.json option = None ]} The library provides both exception-raising and result-returning variants: {v val get : nav t -> Jsont.json -> Jsont.json val get_result : nav t -> Jsont.json -> (Jsont.json, Jsont.Error.t) result val find : nav t -> Jsont.json -> Jsont.json option v} {2 Array Index Rules} {{:https://datatracker.ietf.org/doc/html/rfc6901}RFC 6901} has specific rules for array indices. {{:https://datatracker.ietf.org/doc/html/rfc6901#section-4}Section 4} states: {i characters comprised of digits [...] 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} And importantly: {i note that leading zeros are not allowed} {@ocaml[ # of_string_nav "/foo/0";; - : nav t = [Mem "foo"; Nth 0] ]} Zero itself is fine. {@ocaml[ # of_string_nav "/foo/01";; - : nav t = [Mem "foo"; Mem "01"] ]} But [01] 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. {1 The End-of-Array Marker: [-] and Type Safety} {{:https://datatracker.ietf.org/doc/html/rfc6901#section-4}RFC 6901, Section 4} introduces a special token: {i exactly the single character "-", making the new referenced value the (nonexistent) member after the last array element.} This [-] marker is unique to JSON Pointer (JSON Path has no equivalent). It's primarily useful for JSON Patch operations ({{:https://datatracker.ietf.org/doc/html/rfc6902}RFC 6902}) to append elements to arrays. {2 Navigation vs Append Pointers} The [json-pointer] library uses {b phantom types} to encode the difference between pointers that can be used for navigation and pointers that target the "append position": {v type nav (* A pointer to an existing element *) type append (* A pointer ending with "-" (append position) *) type 'a t (* Pointer with phantom type parameter *) type any (* Existential: wraps either nav or append *) v} When you parse a pointer with {!Json_pointer.of_string}, you get an {!type:Json_pointer.any} pointer that can be used directly with mutation operations: {@ocaml[ # of_string "/foo/0";; - : any = Any # of_string "/foo/-";; - : any = Any ]} The [-] creates an append pointer. The {!type:Json_pointer.any} type wraps either kind, making it ergonomic to use with operations like {!Json_pointer.set} and {!Json_pointer.add}. {2 Why Two Pointer Types?} The RFC explains that [-] refers to a {e nonexistent} position: {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.} So you {b cannot use [get] or [find]} with an append pointer - it makes no sense to retrieve a value from a position that doesn't exist! The library enforces this: - Use {!Json_pointer.of_string_nav} when you need to call {!Json_pointer.get} or {!Json_pointer.find} - Use {!Json_pointer.of_string} (returns {!type:Json_pointer.any}) for mutation operations Mutation operations like {!Json_pointer.add} accept {!type:Json_pointer.any} directly: {x@ocaml[ # let arr_obj = parse_json {|{"foo":["a","b"]}|};; val arr_obj : Jsont.json = {"foo":["a","b"]} # add (of_string "/foo/-") arr_obj ~value:(Jsont.Json.string "c");; - : Jsont.json = {"foo":["a","b","c"]} ]x} For retrieval operations, use {!Json_pointer.of_string_nav} which ensures the pointer doesn't contain [-]: {@ocaml[ # of_string_nav "/foo/0";; - : nav t = [Mem "foo"; Nth 0] # of_string_nav "/foo/-";; Exception: Jsont.Error Invalid JSON Pointer: '-' not allowed in navigation pointer. ]} {2 Creating Append Pointers Programmatically} You can convert a navigation pointer to an append pointer using {!Json_pointer.at_end}: {@ocaml[ # let nav_ptr = of_string_nav "/foo";; val nav_ptr : nav t = [Mem "foo"] # let app_ptr = at_end nav_ptr;; val app_ptr : append t = [Mem "foo"] /- # to_string app_ptr;; - : string = "/foo/-" ]} {1 Mutation Operations} While {{:https://datatracker.ietf.org/doc/html/rfc6901}RFC 6901} defines JSON Pointer for read-only access, {{:https://datatracker.ietf.org/doc/html/rfc6902}RFC 6902} (JSON Patch) uses JSON Pointer for modifications. The [json-pointer] library provides these operations. {2 Add} The {!Json_pointer.add} operation inserts a value at a location. It accepts {!type:Json_pointer.any} pointers, so you can use {!Json_pointer.of_string} directly: {x@ocaml[ # let obj = parse_json {|{"foo":"bar"}|};; val obj : Jsont.json = {"foo":"bar"} # add (of_string "/baz") obj ~value:(Jsont.Json.string "qux");; - : Jsont.json = {"foo":"bar","baz":"qux"} ]x} For arrays, {!Json_pointer.add} inserts BEFORE the specified index: {x@ocaml[ # let arr_obj = parse_json {|{"foo":["a","b"]}|};; val arr_obj : Jsont.json = {"foo":["a","b"]} # add (of_string "/foo/1") arr_obj ~value:(Jsont.Json.string "X");; - : Jsont.json = {"foo":["a","X","b"]} ]x} This is where the [-] marker shines - it appends to the end: {x@ocaml[ # add (of_string "/foo/-") arr_obj ~value:(Jsont.Json.string "c");; - : Jsont.json = {"foo":["a","b","c"]} ]x} You can also use {!Json_pointer.at_end} to create an append pointer programmatically: {x@ocaml[ # add (any (at_end (of_string_nav "/foo"))) arr_obj ~value:(Jsont.Json.string "c");; - : Jsont.json = {"foo":["a","b","c"]} ]x} {2 Ergonomic Mutation with [any]} Since {!Json_pointer.add}, {!Json_pointer.set}, {!Json_pointer.move}, and {!Json_pointer.copy} accept {!type:Json_pointer.any} pointers, you can use {!Json_pointer.of_string} directly without any pattern matching. This makes JSON Patch implementations straightforward: {x@ocaml[ # let items = parse_json {|{"items":["x"]}|};; val items : Jsont.json = {"items":["x"]} # add (of_string "/items/0") items ~value:(Jsont.Json.string "y");; - : Jsont.json = {"items":["y","x"]} # add (of_string "/items/-") items ~value:(Jsont.Json.string "z");; - : Jsont.json = {"items":["x","z"]} ]x} The same pointer works whether it targets an existing position or the append marker - no conditional logic needed. {2 Remove} The {!Json_pointer.remove} operation deletes a value. It only accepts [nav t] because you can only remove something that exists: {x@ocaml[ # let two_fields = parse_json {|{"foo":"bar","baz":"qux"}|};; val two_fields : Jsont.json = {"foo":"bar","baz":"qux"} # remove (of_string_nav "/baz") two_fields ;; - : Jsont.json = {"foo":"bar"} ]x} For arrays, it removes and shifts: {x@ocaml[ # let three_elem = parse_json {|{"foo":["a","b","c"]}|};; val three_elem : Jsont.json = {"foo":["a","b","c"]} # remove (of_string_nav "/foo/1") three_elem ;; - : Jsont.json = {"foo":["a","c"]} ]x} {2 Replace} The {!Json_pointer.replace} operation updates an existing value: {@ocaml[ # replace (of_string_nav "/foo") obj ~value:(Jsont.Json.string "baz") ;; - : Jsont.json = {"foo":"baz"} ]} Unlike {!Json_pointer.add}, {!Json_pointer.replace} requires the target to already exist (hence [nav t]). Attempting to replace a nonexistent path raises an error. {2 Move} The {!Json_pointer.move} operation relocates a value. The source ([from]) must be a [nav t] (you can only move something that exists), but the destination ([path]) accepts {!type:Json_pointer.any}: {x@ocaml[ # let nested = parse_json {|{"foo":{"bar":"baz"},"qux":{}}|};; val nested : Jsont.json = {"foo":{"bar":"baz"},"qux":{}} # move ~from:(of_string_nav "/foo/bar") ~path:(of_string "/qux/thud") nested;; - : Jsont.json = {"foo":{},"qux":{"thud":"baz"}} ]x} {2 Copy} The {!Json_pointer.copy} operation duplicates a value (same typing as {!Json_pointer.move}): {x@ocaml[ # let to_copy = parse_json {|{"foo":{"bar":"baz"}}|};; val to_copy : Jsont.json = {"foo":{"bar":"baz"}} # copy ~from:(of_string_nav "/foo/bar") ~path:(of_string "/foo/qux") to_copy;; - : Jsont.json = {"foo":{"bar":"baz","qux":"baz"}} ]x} {2 Test} The {!Json_pointer.test} operation verifies a value (useful in JSON Patch): {@ocaml[ # test (of_string_nav "/foo") obj ~expected:(Jsont.Json.string "bar");; - : bool = true # test (of_string_nav "/foo") obj ~expected:(Jsont.Json.string "wrong");; - : bool = false ]} {1:escaping Escaping Special Characters} {{:https://datatracker.ietf.org/doc/html/rfc6901#section-3}RFC 6901, Section 3} explains the escaping rules: {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.} Why these specific characters? - [/] separates tokens, so it must be escaped inside a token - [~] is the escape character itself, so it must also be escaped The escape sequences are: - [~0] represents [~] (tilde) - [~1] represents [/] (forward slash) {2 The Library Handles Escaping Automatically} {b Important}: When using [json-pointer] programmatically, you rarely need to think about escaping. The [Mem] variant stores unescaped strings, and escaping happens automatically during serialization: {@ocaml[ # let p = make [mem "a/b"];; val p : nav t = [Mem "a/b"] # to_string p;; - : string = "/a~1b" # of_string_nav "/a~1b";; - : nav t = [Mem "a/b"] ]} {2 Escaping in Action} The {!Json_pointer.Token} module exposes the escaping functions: {@ocaml[ # Token.escape "hello";; - : string = "hello" # Token.escape "a/b";; - : string = "a~1b" # Token.escape "a~b";; - : string = "a~0b" # Token.escape "~/";; - : string = "~0~1" ]} {2 Unescaping} And the reverse process: {@ocaml[ # Token.unescape "a~1b";; - : string = "a/b" # Token.unescape "a~0b";; - : string = "a~b" ]} {2 The Order Matters!} {{:https://datatracker.ietf.org/doc/html/rfc6901#section-4}RFC 6901, Section 4} is careful to specify the unescaping order: {i Evaluation of each reference token begins by decoding any escaped character sequence. This is performed by first transforming any occurrence of the sequence '~1' to '/', and then transforming any occurrence of the sequence '~0' to '~'. By performing the substitutions in this order, an implementation avoids the error of turning '~01' first into '~1' and then into '/', which would be incorrect (the string '~01' correctly becomes '~1' after transformation).} Let's verify this tricky case: {@ocaml[ # Token.unescape "~01";; - : string = "~1" ]} If we unescaped [~0] first, [~01] would become [~1], which would then become [/]. But that's wrong! The sequence [~01] should become the literal string [~1] (a tilde followed by the digit one). {1 URI Fragment Encoding} JSON Pointers can be embedded in URIs. {{:https://datatracker.ietf.org/doc/html/rfc6901#section-6}RFC 6901, Section 6} explains: {i A JSON Pointer can be represented in a URI fragment identifier by encoding it into octets using UTF-8, while percent-encoding those characters not allowed by the fragment rule in {{:https://datatracker.ietf.org/doc/html/rfc3986}RFC 3986}.} This adds percent-encoding on top of the [~0]/[~1] escaping: {@ocaml[ # to_uri_fragment (of_string_nav "/foo");; - : string = "/foo" # to_uri_fragment (of_string_nav "/a~1b");; - : string = "/a~1b" # to_uri_fragment (of_string_nav "/c%d");; - : string = "/c%25d" # to_uri_fragment (of_string_nav "/ ");; - : string = "/%20" ]} The [%] character must be percent-encoded as [%25] in URIs, and spaces become [%20]. Here's the RFC example showing the URI fragment forms: {ul {- [""] -> [#] -> whole document} {- ["/foo"] -> [#/foo] -> [["bar", "baz"]]} {- ["/foo/0"] -> [#/foo/0] -> ["bar"]} {- ["/"] -> [#/] -> [0]} {- ["/a~1b"] -> [#/a~1b] -> [1]} {- ["/c%d"] -> [#/c%25d] -> [2]} {- ["/ "] -> [#/%20] -> [7]} {- ["/m~0n"] -> [#/m~0n] -> [8]} } {1 Building Pointers Programmatically} Instead of parsing strings, you can build pointers from indices: {@ocaml[ # let port_ptr = make [mem "database"; mem "port"];; val port_ptr : nav t = [Mem "database"; Mem "port"] # to_string port_ptr;; - : string = "/database/port" ]} For array access, use the {!Json_pointer.nth} helper: {@ocaml[ # let first_feature_ptr = make [mem "features"; nth 0];; val first_feature_ptr : nav t = [Mem "features"; Nth 0] # to_string first_feature_ptr;; - : string = "/features/0" ]} {2 Pointer Navigation} You can build pointers incrementally using the [/] operator (or {!Json_pointer.append_index}): {@ocaml[ # let db_ptr = of_string_nav "/database";; val db_ptr : nav t = [Mem "database"] # let creds_ptr = db_ptr / mem "credentials";; val creds_ptr : nav t = [Mem "database"; Mem "credentials"] # let user_ptr = creds_ptr / mem "username";; val user_ptr : nav t = [Mem "database"; Mem "credentials"; Mem "username"] # to_string user_ptr;; - : string = "/database/credentials/username" ]} Or concatenate two pointers: {@ocaml[ # let base = of_string_nav "/api/v1";; val base : nav t = [Mem "api"; Mem "v1"] # let endpoint = of_string_nav "/users/0";; val endpoint : nav t = [Mem "users"; Nth 0] # to_string (concat base endpoint);; - : string = "/api/v1/users/0" ]} {1 Jsont Integration} The library integrates with the {!Jsont} codec system, allowing you to combine JSON Pointer navigation with typed decoding. This is powerful because you can point to a location in a JSON document and decode it directly to an OCaml type. {x@ocaml[ # let config_json = parse_json {|{ "database": { "host": "localhost", "port": 5432, "credentials": {"username": "admin", "password": "secret"} }, "features": ["auth", "logging", "metrics"] }|};; val config_json : Jsont.json = {"database":{"host":"localhost","port":5432,"credentials":{"username":"admin","password":"secret"}},"features":["auth","logging","metrics"]} ]x} {2 Typed Access with [path]} The {!Json_pointer.path} combinator combines pointer navigation with typed decoding: {@ocaml[ # let nav = of_string_nav "/database/host";; val nav : nav t = [Mem "database"; Mem "host"] # let db_host = Jsont.Json.decode (path nav Jsont.string) config_json |> Result.get_ok;; val db_host : string = "localhost" # let db_port = Jsont.Json.decode (path (of_string_nav "/database/port") Jsont.int) config_json |> Result.get_ok;; val db_port : int = 5432 ]} Extract a list of strings: {@ocaml[ # let features = Jsont.Json.decode (path (of_string_nav "/features") Jsont.(list string)) config_json |> Result.get_ok;; val features : string list = ["auth"; "logging"; "metrics"] ]} {2 Default Values with [~absent]} Use [~absent] to provide a default when a path doesn't exist: {@ocaml[ # let timeout = Jsont.Json.decode (path ~absent:30 (of_string_nav "/database/timeout") Jsont.int) config_json |> Result.get_ok;; val timeout : int = 30 ]} {2 Nested Path Extraction} You can extract values from deeply nested structures: {x@ocaml[ # let org_json = parse_json {|{ "organization": { "owner": {"name": "Alice", "email": "alice@example.com", "age": 35}, "members": [{"name": "Bob", "email": "bob@example.com", "age": 28}] } }|};; val org_json : Jsont.json = {"organization":{"owner":{"name":"Alice","email":"alice@example.com","age":35},"members":[{"name":"Bob","email":"bob@example.com","age":28}]}} # Jsont.Json.decode (path (of_string_nav "/organization/owner/name") Jsont.string) org_json |> Result.get_ok;; - : string = "Alice" # Jsont.Json.decode (path (of_string_nav "/organization/members/0/age") Jsont.int) org_json |> Result.get_ok;; - : int = 28 ]x} {2 Comparison: Raw vs Typed Access} {b Raw access} requires pattern matching: {@ocaml[ # let raw_port = match get (of_string_nav "/database/port") config_json with | Jsont.Number (f, _) -> int_of_float f | _ -> failwith "expected number";; val raw_port : int = 5432 ]} {b Typed access} is cleaner and type-safe: {@ocaml[ # let typed_port = Jsont.Json.decode (path (of_string_nav "/database/port") Jsont.int) config_json |> Result.get_ok;; val typed_port : int = 5432 ]} The typed approach catches mismatches at decode time with clear errors. {2 Updates with Polymorphic Pointers} The {!Json_pointer.set} and {!Json_pointer.add} functions accept {!type:Json_pointer.any} pointers, which means you can use the result of {!Json_pointer.of_string} directly without pattern matching: {x@ocaml[ # let tasks = parse_json {|{"tasks":["buy milk"]}|};; val tasks : Jsont.json = {"tasks":["buy milk"]} # set (of_string "/tasks/0") tasks ~value:(Jsont.Json.string "buy eggs");; - : Jsont.json = {"tasks":["buy eggs"]} # set (of_string "/tasks/-") tasks ~value:(Jsont.Json.string "call mom");; - : Jsont.json = {"tasks":["buy milk","call mom"]} ]x} This is useful for implementing JSON Patch ({{:https://datatracker.ietf.org/doc/html/rfc6902}RFC 6902}) where operations like ["add"] can target either existing positions or the append marker. If you need to distinguish between pointer types at runtime, use {!Json_pointer.of_string_kind} which returns a polymorphic variant: {x@ocaml[ # of_string_kind "/tasks/0";; - : [ `Append of append t | `Nav of nav t ] = `Nav [Mem "tasks"; Nth 0] # of_string_kind "/tasks/-";; - : [ `Append of append t | `Nav of nav t ] = `Append [Mem "tasks"] /- ]x} {1 Summary} JSON Pointer ({{:https://datatracker.ietf.org/doc/html/rfc6901}RFC 6901}) provides a simple but powerful way to address values within JSON documents: {ol {- {b Syntax}: Pointers are strings of [/]-separated reference tokens} {- {b Escaping}: Use [~0] for [~] and [~1] for [/] in tokens (handled automatically by the library)} {- {b Evaluation}: Tokens navigate through objects (by key) and arrays (by index)} {- {b URI Encoding}: Pointers can be percent-encoded for use in URIs} {- {b Mutations}: Combined with JSON Patch ({{:https://datatracker.ietf.org/doc/html/rfc6902}RFC 6902}), pointers enable structured updates} {- {b Type Safety}: Phantom types ([nav t] vs [append t]) prevent misuse of append pointers with retrieval operations, while the [any] existential type allows ergonomic use with mutation operations} } The [json-pointer] library implements all of this with type-safe OCaml interfaces, integration with the [jsont] codec system, and proper error handling for malformed pointers and missing values. {2 Key Points on JSON Pointer vs JSON Path} {ul {- {b JSON Pointer} addresses a {e single} location (like a file path)} {- {b JSON Path} queries for {e multiple} values (like a search)} {- The [-] token is unique to JSON Pointer - it means "append position" for arrays} {- The library uses phantom types to enforce that [-] (append) pointers cannot be used with [get]/[find]} }