···11+{0 jsont-pointer}
22+33+{!modules: Jsont_pointer Jsont_pointer_top}
44+55+{1 Tutorial}
66+77+See the {!page-tutorial} for a comprehensive guide to using JSON Pointers
88+with this library.
-873
doc/tutorial.md
···11-# JSON Pointer Tutorial
22-33-This tutorial introduces JSON Pointer as defined in
44-[RFC 6901](https://www.rfc-editor.org/rfc/rfc6901), and demonstrates
55-the `jsont-pointer` OCaml library through interactive examples.
66-77-## JSON Pointer vs JSON Path
88-99-Before diving in, it's worth understanding the difference between JSON
1010-Pointer and JSON Path, as they serve different purposes:
1111-1212-**JSON Pointer** (RFC 6901) is an *indicator syntax* that specifies a
1313-*single location* within JSON data. It always identifies at most one
1414-value.
1515-1616-**JSON Path** is a *query syntax* that can *search* JSON data and return
1717-*multiple* values matching specified criteria.
1818-1919-Use JSON Pointer when you need to address a single, specific location
2020-(like JSON Schema's `$ref`). Use JSON Path when you might need multiple
2121-results (like Kubernetes queries).
2222-2323-The `jsont-pointer` library implements JSON Pointer and integrates with
2424-the `Jsont.Path` type for representing navigation indices.
2525-2626-## Setup
2727-2828-First, let's set up our environment with helper functions:
2929-3030-```ocaml
3131-# open Jsont_pointer;;
3232-# #install_printer Jsont_pointer_top.nav_printer;;
3333-# #install_printer Jsont_pointer_top.append_printer;;
3434-# #install_printer Jsont_pointer_top.json_printer;;
3535-# #install_printer Jsont_pointer_top.error_printer;;
3636-# let parse_json s =
3737- match Jsont_bytesrw.decode_string Jsont.json s with
3838- | Ok json -> json
3939- | Error e -> failwith e;;
4040-val parse_json : string -> Jsont.json = <fun>
4141-```
4242-4343-## What is JSON Pointer?
4444-4545-From RFC 6901, Section 1:
4646-4747-> JSON Pointer defines a string syntax for identifying a specific value
4848-> within a JavaScript Object Notation (JSON) document.
4949-5050-In other words, JSON Pointer is an addressing scheme for locating values
5151-inside a JSON structure. Think of it like a filesystem path, but for JSON
5252-documents instead of files.
5353-5454-For example, given this JSON document:
5555-5656-```ocaml
5757-# let users_json = parse_json {|{
5858- "users": [
5959- {"name": "Alice", "age": 30},
6060- {"name": "Bob", "age": 25}
6161- ]
6262- }|};;
6363-val users_json : Jsont.json =
6464- {"users":[{"name":"Alice","age":30},{"name":"Bob","age":25}]}
6565-```
6666-6767-The JSON Pointer `/users/0/name` refers to the string `"Alice"`:
6868-6969-```ocaml
7070-# let ptr = of_string_nav "/users/0/name";;
7171-val ptr : nav t = [Mem "users"; Nth 0; Mem "name"]
7272-# get ptr users_json;;
7373-- : Jsont.json = "Alice"
7474-```
7575-7676-In OCaml, this is represented by the `'a Jsont_pointer.t` type - a sequence
7777-of navigation steps from the document root to a target value. The phantom
7878-type parameter `'a` encodes whether this is a navigation pointer or an
7979-append pointer (more on this later).
8080-8181-## Syntax: Reference Tokens
8282-8383-RFC 6901, Section 3 defines the syntax:
8484-8585-> A JSON Pointer is a Unicode string containing a sequence of zero or more
8686-> reference tokens, each prefixed by a '/' (%x2F) character.
8787-8888-The grammar is elegantly simple:
8989-9090-```
9191-json-pointer = *( "/" reference-token )
9292-reference-token = *( unescaped / escaped )
9393-```
9494-9595-This means:
9696-- The empty string `""` is a valid pointer (it refers to the whole document)
9797-- Every non-empty pointer starts with `/`
9898-- Everything between `/` characters is a "reference token"
9999-100100-Let's see this in action:
101101-102102-```ocaml
103103-# of_string_nav "";;
104104-- : nav t = []
105105-```
106106-107107-The empty pointer has no reference tokens - it points to the root.
108108-109109-```ocaml
110110-# of_string_nav "/foo";;
111111-- : nav t = [Mem "foo"]
112112-```
113113-114114-The pointer `/foo` has one token: `foo`. Since it's not a number, it's
115115-interpreted as an object member name (`Mem`).
116116-117117-```ocaml
118118-# of_string_nav "/foo/0";;
119119-- : nav t = [Mem "foo"; Nth 0]
120120-```
121121-122122-Here we have two tokens: `foo` (a member name) and `0` (interpreted as
123123-an array index `Nth`).
124124-125125-```ocaml
126126-# of_string_nav "/foo/bar/baz";;
127127-- : nav t = [Mem "foo"; Mem "bar"; Mem "baz"]
128128-```
129129-130130-Multiple tokens navigate deeper into nested structures.
131131-132132-### The Index Type
133133-134134-Each reference token is represented using `Jsont.Path.index`:
135135-136136-<!-- $MDX skip -->
137137-```ocaml
138138-type index = Jsont.Path.index
139139-(* = Jsont.Path.Mem of string * Jsont.Meta.t
140140- | Jsont.Path.Nth of int * Jsont.Meta.t *)
141141-```
142142-143143-The `Mem` constructor is for object member access, and `Nth` is for array
144144-index access. The member name is **unescaped** - you work with the actual
145145-key string (like `"a/b"`) and the library handles any escaping needed
146146-for the JSON Pointer string representation.
147147-148148-### Invalid Syntax
149149-150150-What happens if a pointer doesn't start with `/`?
151151-152152-```ocaml
153153-# of_string_nav "foo";;
154154-Exception:
155155-Jsont.Error Invalid JSON Pointer: must be empty or start with '/': foo.
156156-```
157157-158158-The RFC is strict: non-empty pointers MUST start with `/`.
159159-160160-For safer parsing, use `of_string_result`:
161161-162162-```ocaml
163163-# of_string_result "foo";;
164164-- : ([ `Append of append t | `Nav of nav t ], string) result =
165165-Error "Invalid JSON Pointer: must be empty or start with '/': foo"
166166-# of_string_result "/valid";;
167167-- : ([ `Append of append t | `Nav of nav t ], string) result =
168168-Ok (`Nav [Mem "valid"])
169169-```
170170-171171-## Evaluation: Navigating JSON
172172-173173-Now we come to the heart of JSON Pointer: evaluation. RFC 6901, Section 4
174174-describes how a pointer is resolved against a JSON document:
175175-176176-> Evaluation of a JSON Pointer begins with a reference to the root value
177177-> of a JSON document and completes with a reference to some value within
178178-> the document. Each reference token in the JSON Pointer is evaluated
179179-> sequentially.
180180-181181-Let's use the example JSON document from RFC 6901, Section 5:
182182-183183-```ocaml
184184-# let rfc_example = parse_json {|{
185185- "foo": ["bar", "baz"],
186186- "": 0,
187187- "a/b": 1,
188188- "c%d": 2,
189189- "e^f": 3,
190190- "g|h": 4,
191191- "i\\j": 5,
192192- "k\"l": 6,
193193- " ": 7,
194194- "m~n": 8
195195- }|};;
196196-val rfc_example : Jsont.json =
197197- {"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}
198198-```
199199-200200-This document is carefully constructed to exercise various edge cases!
201201-202202-### The Root Pointer
203203-204204-```ocaml
205205-# get root rfc_example ;;
206206-- : Jsont.json =
207207-{"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}
208208-```
209209-210210-The empty pointer (`root`) returns the whole document.
211211-212212-### Object Member Access
213213-214214-```ocaml
215215-# get (of_string_nav "/foo") rfc_example ;;
216216-- : Jsont.json = ["bar","baz"]
217217-```
218218-219219-`/foo` accesses the member named `foo`, which is an array.
220220-221221-### Array Index Access
222222-223223-```ocaml
224224-# get (of_string_nav "/foo/0") rfc_example ;;
225225-- : Jsont.json = "bar"
226226-# get (of_string_nav "/foo/1") rfc_example ;;
227227-- : Jsont.json = "baz"
228228-```
229229-230230-`/foo/0` first goes to `foo`, then accesses index 0 of the array.
231231-232232-### Empty String as Key
233233-234234-JSON allows empty strings as object keys:
235235-236236-```ocaml
237237-# get (of_string_nav "/") rfc_example ;;
238238-- : Jsont.json = 0
239239-```
240240-241241-The pointer `/` has one token: the empty string. This accesses the member
242242-with an empty name.
243243-244244-### Keys with Special Characters
245245-246246-The RFC example includes keys with `/` and `~` characters:
247247-248248-```ocaml
249249-# get (of_string_nav "/a~1b") rfc_example ;;
250250-- : Jsont.json = 1
251251-```
252252-253253-The token `a~1b` refers to the key `a/b`. We'll explain this escaping
254254-[below](#escaping-special-characters).
255255-256256-```ocaml
257257-# get (of_string_nav "/m~0n") rfc_example ;;
258258-- : Jsont.json = 8
259259-```
260260-261261-The token `m~0n` refers to the key `m~n`.
262262-263263-**Important**: When using the OCaml library programmatically, you don't need
264264-to worry about escaping. The `Mem` variant holds the literal key name:
265265-266266-```ocaml
267267-# let slash_ptr = make [mem "a/b"];;
268268-val slash_ptr : nav t = [Mem "a/b"]
269269-# to_string slash_ptr;;
270270-- : string = "/a~1b"
271271-# get slash_ptr rfc_example ;;
272272-- : Jsont.json = 1
273273-```
274274-275275-The library escapes it when converting to string.
276276-277277-### Other Special Characters (No Escaping Needed)
278278-279279-Most characters don't need escaping in JSON Pointer strings:
280280-281281-```ocaml
282282-# get (of_string_nav "/c%d") rfc_example ;;
283283-- : Jsont.json = 2
284284-# get (of_string_nav "/e^f") rfc_example ;;
285285-- : Jsont.json = 3
286286-# get (of_string_nav "/g|h") rfc_example ;;
287287-- : Jsont.json = 4
288288-# get (of_string_nav "/ ") rfc_example ;;
289289-- : Jsont.json = 7
290290-```
291291-292292-Even a space is a valid key character!
293293-294294-### Error Conditions
295295-296296-What happens when we try to access something that doesn't exist?
297297-298298-```ocaml
299299-# get_result (of_string_nav "/nonexistent") rfc_example;;
300300-- : (Jsont.json, Jsont.Error.t) result =
301301-Error JSON Pointer: member 'nonexistent' not found
302302-File "-":
303303-# find (of_string_nav "/nonexistent") rfc_example;;
304304-- : Jsont.json option = None
305305-```
306306-307307-Or an out-of-bounds array index:
308308-309309-```ocaml
310310-# find (of_string_nav "/foo/99") rfc_example;;
311311-- : Jsont.json option = None
312312-```
313313-314314-Or try to index into a non-container:
315315-316316-```ocaml
317317-# find (of_string_nav "/foo/0/invalid") rfc_example;;
318318-- : Jsont.json option = None
319319-```
320320-321321-The library provides both exception-raising and result-returning variants:
322322-323323-<!-- $MDX skip -->
324324-```ocaml
325325-val get : nav t -> Jsont.json -> Jsont.json
326326-val get_result : nav t -> Jsont.json -> (Jsont.json, Jsont.Error.t) result
327327-val find : nav t -> Jsont.json -> Jsont.json option
328328-```
329329-330330-### Array Index Rules
331331-332332-RFC 6901 has specific rules for array indices. Section 4 states:
333333-334334-> characters comprised of digits [...] that represent an unsigned base-10
335335-> integer value, making the new referenced value the array element with
336336-> the zero-based index identified by the token
337337-338338-And importantly:
339339-340340-> note that leading zeros are not allowed
341341-342342-```ocaml
343343-# of_string_nav "/foo/0";;
344344-- : nav t = [Mem "foo"; Nth 0]
345345-```
346346-347347-Zero itself is fine.
348348-349349-```ocaml
350350-# of_string_nav "/foo/01";;
351351-- : nav t = [Mem "foo"; Mem "01"]
352352-```
353353-354354-But `01` has a leading zero, so it's NOT treated as an array index - it
355355-becomes a member name instead. This protects against accidental octal
356356-interpretation.
357357-358358-## The End-of-Array Marker: `-` and Type Safety
359359-360360-RFC 6901, Section 4 introduces a special token:
361361-362362-> exactly the single character "-", making the new referenced value the
363363-> (nonexistent) member after the last array element.
364364-365365-This `-` marker is unique to JSON Pointer (JSON Path has no equivalent).
366366-It's primarily useful for JSON Patch operations (RFC 6902) to append
367367-elements to arrays.
368368-369369-### Navigation vs Append Pointers
370370-371371-The `jsont-pointer` library uses **phantom types** to encode the difference
372372-between pointers that can be used for navigation and pointers that target
373373-the "append position":
374374-375375-<!-- $MDX skip -->
376376-```ocaml
377377-type nav (* A pointer to an existing element *)
378378-type append (* A pointer ending with "-" (append position) *)
379379-type 'a t (* Pointer with phantom type parameter *)
380380-```
381381-382382-When you parse a pointer, you get either a `nav t` or an `append t`:
383383-384384-```ocaml
385385-# of_string "/foo/0";;
386386-- : [ `Append of append t | `Nav of nav t ] = `Nav [Mem "foo"; Nth 0]
387387-# of_string "/foo/-";;
388388-- : [ `Append of append t | `Nav of nav t ] = `Append [Mem "foo"] /-
389389-```
390390-391391-The `-` creates an `append` pointer. Note that in the internal
392392-representation, the append position is tracked separately (shown as `/-`).
393393-394394-### Why Phantom Types?
395395-396396-The RFC explains that `-` refers to a *nonexistent* position:
397397-398398-> Note that the use of the "-" character to index an array will always
399399-> result in such an error condition because by definition it refers to
400400-> a nonexistent array element.
401401-402402-So you **cannot use `get` or `find`** with an append pointer - it makes
403403-no sense to retrieve a value from a position that doesn't exist! The
404404-library enforces this at compile time:
405405-406406-```ocaml
407407-# (* This won't compile: get requires nav t, not append t *)
408408- (* get (match of_string "/foo/-" with `Append p -> p | _ -> assert false) rfc_example;; *)
409409-```
410410-411411-However, append pointers **are** valid for mutation operations like `add`:
412412-413413-```ocaml
414414-# let arr_obj = parse_json {|{"foo":["a","b"]}|};;
415415-val arr_obj : Jsont.json = {"foo":["a","b"]}
416416-# match of_string "/foo/-" with
417417- | `Append p -> add p arr_obj ~value:(Jsont.Json.string "c")
418418- | `Nav _ -> assert false ;;
419419-- : Jsont.json = {"foo":["a","b","c"]}
420420-```
421421-422422-For convenience, use `of_string_nav` when you know a pointer shouldn't
423423-contain `-`:
424424-425425-```ocaml
426426-# of_string_nav "/foo/0";;
427427-- : nav t = [Mem "foo"; Nth 0]
428428-# of_string_nav "/foo/-";;
429429-Exception:
430430-Jsont.Error Invalid JSON Pointer: '-' not allowed in navigation pointer.
431431-```
432432-433433-### Creating Append Pointers Programmatically
434434-435435-You can convert a navigation pointer to an append pointer using `at_end`:
436436-437437-```ocaml
438438-# let nav_ptr = of_string_nav "/foo";;
439439-val nav_ptr : nav t = [Mem "foo"]
440440-# let app_ptr = at_end nav_ptr;;
441441-val app_ptr : append t = [Mem "foo"] /-
442442-# to_string app_ptr;;
443443-- : string = "/foo/-"
444444-```
445445-446446-## Mutation Operations
447447-448448-While RFC 6901 defines JSON Pointer for read-only access, RFC 6902
449449-(JSON Patch) uses JSON Pointer for modifications. The `jsont-pointer`
450450-library provides these operations.
451451-452452-### Which Pointer Type for Which Operation?
453453-454454-The phantom type system enforces correct usage:
455455-456456-| Operation | Accepts | Because |
457457-|-----------|---------|---------|
458458-| `get`, `find` | `nav t` only | Can't retrieve from non-existent position |
459459-| `remove` | `nav t` only | Can't remove what doesn't exist |
460460-| `replace` | `nav t` only | Can't replace what doesn't exist |
461461-| `test` | `nav t` only | Can't test non-existent position |
462462-| `add` | `_ t` (both) | Can add at existing position OR append |
463463-| `set` | `_ t` (both) | Can set existing position OR append |
464464-| `move`, `copy` | `from:nav t`, `path:_ t` | Source must exist, dest can be append |
465465-466466-### Add
467467-468468-The `add` operation inserts a value at a location:
469469-470470-```ocaml
471471-# let obj = parse_json {|{"foo":"bar"}|};;
472472-val obj : Jsont.json = {"foo":"bar"}
473473-# add (of_string_nav "/baz") obj ~value:(Jsont.Json.string "qux")
474474- ;;
475475-- : Jsont.json = {"foo":"bar","baz":"qux"}
476476-```
477477-478478-For arrays, `add` inserts BEFORE the specified index:
479479-480480-```ocaml
481481-# let arr_obj = parse_json {|{"foo":["a","b"]}|};;
482482-val arr_obj : Jsont.json = {"foo":["a","b"]}
483483-# add (of_string_nav "/foo/1") arr_obj ~value:(Jsont.Json.string "X")
484484- ;;
485485-- : Jsont.json = {"foo":["a","X","b"]}
486486-```
487487-488488-This is where the `-` marker and append pointers shine - they append to the end:
489489-490490-```ocaml
491491-# match of_string "/foo/-" with
492492- | `Append p -> add p arr_obj ~value:(Jsont.Json.string "c")
493493- | `Nav _ -> assert false ;;
494494-- : Jsont.json = {"foo":["a","b","c"]}
495495-```
496496-497497-Or more conveniently using `at_end`:
498498-499499-```ocaml
500500-# add (at_end (of_string_nav "/foo")) arr_obj ~value:(Jsont.Json.string "c")
501501- ;;
502502-- : Jsont.json = {"foo":["a","b","c"]}
503503-```
504504-505505-### Remove
506506-507507-The `remove` operation deletes a value. It only accepts `nav t` because
508508-you can only remove something that exists:
509509-510510-```ocaml
511511-# let two_fields = parse_json {|{"foo":"bar","baz":"qux"}|};;
512512-val two_fields : Jsont.json = {"foo":"bar","baz":"qux"}
513513-# remove (of_string_nav "/baz") two_fields ;;
514514-- : Jsont.json = {"foo":"bar"}
515515-```
516516-517517-For arrays, it removes and shifts:
518518-519519-```ocaml
520520-# let three_elem = parse_json {|{"foo":["a","b","c"]}|};;
521521-val three_elem : Jsont.json = {"foo":["a","b","c"]}
522522-# remove (of_string_nav "/foo/1") three_elem ;;
523523-- : Jsont.json = {"foo":["a","c"]}
524524-```
525525-526526-### Replace
527527-528528-The `replace` operation updates an existing value:
529529-530530-```ocaml
531531-# replace (of_string_nav "/foo") obj ~value:(Jsont.Json.string "baz")
532532- ;;
533533-- : Jsont.json = {"foo":"baz"}
534534-```
535535-536536-Unlike `add`, `replace` requires the target to already exist (hence `nav t`).
537537-Attempting to replace a nonexistent path raises an error.
538538-539539-### Move
540540-541541-The `move` operation relocates a value. The source (`from`) must be a `nav t`
542542-(you can only move something that exists), but the destination (`path`) can
543543-be either:
544544-545545-```ocaml
546546-# let nested = parse_json {|{"foo":{"bar":"baz"},"qux":{}}|};;
547547-val nested : Jsont.json = {"foo":{"bar":"baz"},"qux":{}}
548548-# move ~from:(of_string_nav "/foo/bar") ~path:(of_string_nav "/qux/thud") nested
549549- ;;
550550-- : Jsont.json = {"foo":{},"qux":{"thud":"baz"}}
551551-```
552552-553553-### Copy
554554-555555-The `copy` operation duplicates a value (same typing as `move`):
556556-557557-```ocaml
558558-# let to_copy = parse_json {|{"foo":{"bar":"baz"}}|};;
559559-val to_copy : Jsont.json = {"foo":{"bar":"baz"}}
560560-# copy ~from:(of_string_nav "/foo/bar") ~path:(of_string_nav "/foo/qux") to_copy
561561- ;;
562562-- : Jsont.json = {"foo":{"bar":"baz","qux":"baz"}}
563563-```
564564-565565-### Test
566566-567567-The `test` operation verifies a value (useful in JSON Patch):
568568-569569-```ocaml
570570-# test (of_string_nav "/foo") obj ~expected:(Jsont.Json.string "bar");;
571571-- : bool = true
572572-# test (of_string_nav "/foo") obj ~expected:(Jsont.Json.string "wrong");;
573573-- : bool = false
574574-```
575575-576576-## Escaping Special Characters
577577-578578-RFC 6901, Section 3 explains the escaping rules:
579579-580580-> Because the characters '\~' (%x7E) and '/' (%x2F) have special meanings
581581-> in JSON Pointer, '\~' needs to be encoded as '\~0' and '/' needs to be
582582-> encoded as '\~1' when these characters appear in a reference token.
583583-584584-Why these specific characters?
585585-- `/` separates tokens, so it must be escaped inside a token
586586-- `~` is the escape character itself, so it must also be escaped
587587-588588-The escape sequences are:
589589-- `~0` represents `~` (tilde)
590590-- `~1` represents `/` (forward slash)
591591-592592-### The Library Handles Escaping Automatically
593593-594594-**Important**: When using `jsont-pointer` programmatically, you rarely need
595595-to think about escaping. The `Mem` variant stores unescaped strings,
596596-and escaping happens automatically during serialization:
597597-598598-```ocaml
599599-# let p = make [mem "a/b"];;
600600-val p : nav t = [Mem "a/b"]
601601-# to_string p;;
602602-- : string = "/a~1b"
603603-# of_string_nav "/a~1b";;
604604-- : nav t = [Mem "a/b"]
605605-```
606606-607607-### Escaping in Action
608608-609609-The `Token` module exposes the escaping functions:
610610-611611-```ocaml
612612-# Token.escape "hello";;
613613-- : string = "hello"
614614-# Token.escape "a/b";;
615615-- : string = "a~1b"
616616-# Token.escape "a~b";;
617617-- : string = "a~0b"
618618-# Token.escape "~/";;
619619-- : string = "~0~1"
620620-```
621621-622622-### Unescaping
623623-624624-And the reverse process:
625625-626626-```ocaml
627627-# Token.unescape "a~1b";;
628628-- : string = "a/b"
629629-# Token.unescape "a~0b";;
630630-- : string = "a~b"
631631-```
632632-633633-### The Order Matters!
634634-635635-RFC 6901, Section 4 is careful to specify the unescaping order:
636636-637637-> Evaluation of each reference token begins by decoding any escaped
638638-> character sequence. This is performed by first transforming any
639639-> occurrence of the sequence '~1' to '/', and then transforming any
640640-> occurrence of the sequence '~0' to '~'. By performing the substitutions
641641-> in this order, an implementation avoids the error of turning '~01' first
642642-> into '~1' and then into '/', which would be incorrect (the string '~01'
643643-> correctly becomes '~1' after transformation).
644644-645645-Let's verify this tricky case:
646646-647647-```ocaml
648648-# Token.unescape "~01";;
649649-- : string = "~1"
650650-```
651651-652652-If we unescaped `~0` first, `~01` would become `~1`, which would then become
653653-`/`. But that's wrong! The sequence `~01` should become the literal string
654654-`~1` (a tilde followed by the digit one).
655655-656656-## URI Fragment Encoding
657657-658658-JSON Pointers can be embedded in URIs. RFC 6901, Section 6 explains:
659659-660660-> A JSON Pointer can be represented in a URI fragment identifier by
661661-> encoding it into octets using UTF-8, while percent-encoding those
662662-> characters not allowed by the fragment rule in RFC 3986.
663663-664664-This adds percent-encoding on top of the `~0`/`~1` escaping:
665665-666666-```ocaml
667667-# to_uri_fragment (of_string_nav "/foo");;
668668-- : string = "/foo"
669669-# to_uri_fragment (of_string_nav "/a~1b");;
670670-- : string = "/a~1b"
671671-# to_uri_fragment (of_string_nav "/c%d");;
672672-- : string = "/c%25d"
673673-# to_uri_fragment (of_string_nav "/ ");;
674674-- : string = "/%20"
675675-```
676676-677677-The `%` character must be percent-encoded as `%25` in URIs, and
678678-spaces become `%20`.
679679-680680-Here's the RFC example showing the URI fragment forms:
681681-682682-| JSON Pointer | URI Fragment | Value |
683683-|-------------|-------------|-------|
684684-| `""` | `#` | whole document |
685685-| `"/foo"` | `#/foo` | `["bar", "baz"]` |
686686-| `"/foo/0"` | `#/foo/0` | `"bar"` |
687687-| `"/"` | `#/` | `0` |
688688-| `"/a~1b"` | `#/a~1b` | `1` |
689689-| `"/c%d"` | `#/c%25d` | `2` |
690690-| `"/ "` | `#/%20` | `7` |
691691-| `"/m~0n"` | `#/m~0n` | `8` |
692692-693693-## Building Pointers Programmatically
694694-695695-Instead of parsing strings, you can build pointers from indices:
696696-697697-```ocaml
698698-# let port_ptr = make [mem "database"; mem "port"];;
699699-val port_ptr : nav t = [Mem "database"; Mem "port"]
700700-# to_string port_ptr;;
701701-- : string = "/database/port"
702702-```
703703-704704-For array access, use the `nth` helper:
705705-706706-```ocaml
707707-# let first_feature_ptr = make [mem "features"; nth 0];;
708708-val first_feature_ptr : nav t = [Mem "features"; Nth 0]
709709-# to_string first_feature_ptr;;
710710-- : string = "/features/0"
711711-```
712712-713713-### Pointer Navigation
714714-715715-You can build pointers incrementally using the `/` operator (or `append_index`):
716716-717717-```ocaml
718718-# let db_ptr = of_string_nav "/database";;
719719-val db_ptr : nav t = [Mem "database"]
720720-# let creds_ptr = db_ptr / mem "credentials";;
721721-val creds_ptr : nav t = [Mem "database"; Mem "credentials"]
722722-# let user_ptr = creds_ptr / mem "username";;
723723-val user_ptr : nav t = [Mem "database"; Mem "credentials"; Mem "username"]
724724-# to_string user_ptr;;
725725-- : string = "/database/credentials/username"
726726-```
727727-728728-Or concatenate two pointers:
729729-730730-```ocaml
731731-# let base = of_string_nav "/api/v1";;
732732-val base : nav t = [Mem "api"; Mem "v1"]
733733-# let endpoint = of_string_nav "/users/0";;
734734-val endpoint : nav t = [Mem "users"; Nth 0]
735735-# to_string (concat base endpoint);;
736736-- : string = "/api/v1/users/0"
737737-```
738738-739739-## Jsont Integration
740740-741741-The library integrates with the `Jsont` codec system, allowing you to
742742-combine JSON Pointer navigation with typed decoding. This is powerful
743743-because you can point to a location in a JSON document and decode it
744744-directly to an OCaml type.
745745-746746-```ocaml
747747-# let config_json = parse_json {|{
748748- "database": {
749749- "host": "localhost",
750750- "port": 5432,
751751- "credentials": {"username": "admin", "password": "secret"}
752752- },
753753- "features": ["auth", "logging", "metrics"]
754754- }|};;
755755-val config_json : Jsont.json =
756756- {"database":{"host":"localhost","port":5432,"credentials":{"username":"admin","password":"secret"}},"features":["auth","logging","metrics"]}
757757-```
758758-759759-### Typed Access with `path`
760760-761761-The `path` combinator combines pointer navigation with typed decoding:
762762-763763-```ocaml
764764-# let db_host =
765765- Jsont.Json.decode
766766- (path (of_string_nav "/database/host") Jsont.string)
767767- config_json
768768- |> Result.get_ok;;
769769-val db_host : string = "localhost"
770770-# let db_port =
771771- Jsont.Json.decode
772772- (path (of_string_nav "/database/port") Jsont.int)
773773- config_json
774774- |> Result.get_ok;;
775775-val db_port : int = 5432
776776-```
777777-778778-Extract a list of strings:
779779-780780-```ocaml
781781-# let features =
782782- Jsont.Json.decode
783783- (path (of_string_nav "/features") Jsont.(list string))
784784- config_json
785785- |> Result.get_ok;;
786786-val features : string list = ["auth"; "logging"; "metrics"]
787787-```
788788-789789-### Default Values with `~absent`
790790-791791-Use `~absent` to provide a default when a path doesn't exist:
792792-793793-```ocaml
794794-# let timeout =
795795- Jsont.Json.decode
796796- (path ~absent:30 (of_string_nav "/database/timeout") Jsont.int)
797797- config_json
798798- |> Result.get_ok;;
799799-val timeout : int = 30
800800-```
801801-802802-### Nested Path Extraction
803803-804804-You can extract values from deeply nested structures:
805805-806806-```ocaml
807807-# let org_json = parse_json {|{
808808- "organization": {
809809- "owner": {"name": "Alice", "email": "alice@example.com", "age": 35},
810810- "members": [{"name": "Bob", "email": "bob@example.com", "age": 28}]
811811- }
812812- }|};;
813813-val org_json : Jsont.json =
814814- {"organization":{"owner":{"name":"Alice","email":"alice@example.com","age":35},"members":[{"name":"Bob","email":"bob@example.com","age":28}]}}
815815-# Jsont.Json.decode
816816- (path (of_string_nav "/organization/owner/name") Jsont.string)
817817- org_json
818818- |> Result.get_ok;;
819819-- : string = "Alice"
820820-# Jsont.Json.decode
821821- (path (of_string_nav "/organization/members/0/age") Jsont.int)
822822- org_json
823823- |> Result.get_ok;;
824824-- : int = 28
825825-```
826826-827827-### Comparison: Raw vs Typed Access
828828-829829-**Raw access** requires pattern matching:
830830-831831-```ocaml
832832-# let raw_port =
833833- match get (of_string_nav "/database/port") config_json with
834834- | Jsont.Number (f, _) -> int_of_float f
835835- | _ -> failwith "expected number";;
836836-val raw_port : int = 5432
837837-```
838838-839839-**Typed access** is cleaner and type-safe:
840840-841841-```ocaml
842842-# let typed_port =
843843- Jsont.Json.decode
844844- (path (of_string_nav "/database/port") Jsont.int)
845845- config_json
846846- |> Result.get_ok;;
847847-val typed_port : int = 5432
848848-```
849849-850850-The typed approach catches mismatches at decode time with clear errors.
851851-852852-## Summary
853853-854854-JSON Pointer (RFC 6901) provides a simple but powerful way to address
855855-values within JSON documents:
856856-857857-1. **Syntax**: Pointers are strings of `/`-separated reference tokens
858858-2. **Escaping**: Use `~0` for `~` and `~1` for `/` in tokens (handled automatically by the library)
859859-3. **Evaluation**: Tokens navigate through objects (by key) and arrays (by index)
860860-4. **URI Encoding**: Pointers can be percent-encoded for use in URIs
861861-5. **Mutations**: Combined with JSON Patch (RFC 6902), pointers enable structured updates
862862-6. **Type Safety**: Phantom types (`nav t` vs `append t`) prevent misuse of append pointers with retrieval operations
863863-864864-The `jsont-pointer` library implements all of this with type-safe OCaml
865865-interfaces, integration with the `jsont` codec system, and proper error
866866-handling for malformed pointers and missing values.
867867-868868-### Key Points on JSON Pointer vs JSON Path
869869-870870-- **JSON Pointer** addresses a *single* location (like a file path)
871871-- **JSON Path** queries for *multiple* values (like a search)
872872-- The `-` token is unique to JSON Pointer - it means "append position" for arrays
873873-- The library uses phantom types to enforce that `-` (append) pointers cannot be used with `get`/`find`
+846
doc/tutorial.mld
···11+{0 JSON Pointer Tutorial}
22+33+This tutorial introduces JSON Pointer as defined in
44+{{:https://www.rfc-editor.org/rfc/rfc6901} RFC 6901}, and demonstrates
55+the [jsont-pointer] OCaml library through interactive examples.
66+77+{1 JSON Pointer vs JSON Path}
88+99+Before diving in, it's worth understanding the difference between JSON
1010+Pointer and JSON Path, as they serve different purposes:
1111+1212+{b JSON Pointer} (RFC 6901) is an {e indicator syntax} that specifies a
1313+{e single location} within JSON data. It always identifies at most one
1414+value.
1515+1616+{b JSON Path} is a {e query syntax} that can {e search} JSON data and return
1717+{e multiple} values matching specified criteria.
1818+1919+Use JSON Pointer when you need to address a single, specific location
2020+(like JSON Schema's [$ref]). Use JSON Path when you might need multiple
2121+results (like Kubernetes queries).
2222+2323+The [jsont-pointer] library implements JSON Pointer and integrates with
2424+the {!Jsont.Path} type for representing navigation indices.
2525+2626+{1 Setup}
2727+2828+First, let's set up our environment. In the toplevel, you can load the
2929+library with [#require "jsont-pointer.top";;] which will automatically
3030+install pretty printers.
3131+3232+{@ocaml[
3333+# Jsont_pointer_top.install ();;
3434+- : unit = ()
3535+# open Jsont_pointer;;
3636+# let parse_json s =
3737+ match Jsont_bytesrw.decode_string Jsont.json s with
3838+ | Ok json -> json
3939+ | Error e -> failwith e;;
4040+val parse_json : string -> Jsont.json = <fun>
4141+]}
4242+4343+{1 What is JSON Pointer?}
4444+4545+From RFC 6901, Section 1:
4646+4747+{i JSON Pointer defines a string syntax for identifying a specific value
4848+within a JavaScript Object Notation (JSON) document.}
4949+5050+In other words, JSON Pointer is an addressing scheme for locating values
5151+inside a JSON structure. Think of it like a filesystem path, but for JSON
5252+documents instead of files.
5353+5454+For example, given this JSON document:
5555+5656+{x@ocaml[
5757+# let users_json =
5858+ parse_json "{\"users\":[{\"name\":\"Alice\",\"age\":30},{\"name\":\"Bob\",\"age\":25}]}";;
5959+val users_json : Jsont.json =
6060+ {"users":[{"name":"Alice","age":30},{"name":"Bob","age":25}]}
6161+]x}
6262+6363+The JSON Pointer [/users/0/name] refers to the string ["Alice"]:
6464+6565+{@ocaml[
6666+# let ptr = of_string_nav "/users/0/name";;
6767+val ptr : nav t = [Mem "users"; Nth 0; Mem "name"]
6868+# get ptr users_json;;
6969+- : Jsont.json = "Alice"
7070+]}
7171+7272+In OCaml, this is represented by the ['a Jsont_pointer.t] type - a sequence
7373+of navigation steps from the document root to a target value. The phantom
7474+type parameter ['a] encodes whether this is a navigation pointer or an
7575+append pointer (more on this later).
7676+7777+{1 Syntax: Reference Tokens}
7878+7979+RFC 6901, Section 3 defines the syntax:
8080+8181+{i A JSON Pointer is a Unicode string containing a sequence of zero or more
8282+reference tokens, each prefixed by a '/' (%x2F) character.}
8383+8484+The grammar is elegantly simple:
8585+8686+{v
8787+json-pointer = *( "/" reference-token )
8888+reference-token = *( unescaped / escaped )
8989+v}
9090+9191+This means:
9292+- The empty string [""] is a valid pointer (it refers to the whole document)
9393+- Every non-empty pointer starts with [/]
9494+- Everything between [/] characters is a "reference token"
9595+9696+Let's see this in action:
9797+9898+{@ocaml[
9999+# of_string_nav "";;
100100+- : nav t = []
101101+]}
102102+103103+The empty pointer has no reference tokens - it points to the root.
104104+105105+{@ocaml[
106106+# of_string_nav "/foo";;
107107+- : nav t = [Mem "foo"]
108108+]}
109109+110110+The pointer [/foo] has one token: [foo]. Since it's not a number, it's
111111+interpreted as an object member name ([Mem]).
112112+113113+{@ocaml[
114114+# of_string_nav "/foo/0";;
115115+- : nav t = [Mem "foo"; Nth 0]
116116+]}
117117+118118+Here we have two tokens: [foo] (a member name) and [0] (interpreted as
119119+an array index [Nth]).
120120+121121+{@ocaml[
122122+# of_string_nav "/foo/bar/baz";;
123123+- : nav t = [Mem "foo"; Mem "bar"; Mem "baz"]
124124+]}
125125+126126+Multiple tokens navigate deeper into nested structures.
127127+128128+{2 The Index Type}
129129+130130+Each reference token is represented using {!Jsont.Path.index}:
131131+132132+{[
133133+type index = Jsont.Path.index
134134+(* = Jsont.Path.Mem of string * Jsont.Meta.t
135135+ | Jsont.Path.Nth of int * Jsont.Meta.t *)
136136+]}
137137+138138+The [Mem] constructor is for object member access, and [Nth] is for array
139139+index access. The member name is {b unescaped} - you work with the actual
140140+key string (like ["a/b"]) and the library handles any escaping needed
141141+for the JSON Pointer string representation.
142142+143143+{2 Invalid Syntax}
144144+145145+What happens if a pointer doesn't start with [/]?
146146+147147+{@ocaml[
148148+# of_string_nav "foo";;
149149+Exception:
150150+Jsont.Error Invalid JSON Pointer: must be empty or start with '/': foo.
151151+]}
152152+153153+The RFC is strict: non-empty pointers MUST start with [/].
154154+155155+For safer parsing, use [of_string_result]:
156156+157157+{@ocaml[
158158+# of_string_result "foo";;
159159+- : ([ `Append of append t | `Nav of nav t ], string) result =
160160+Error "Invalid JSON Pointer: must be empty or start with '/': foo"
161161+# of_string_result "/valid";;
162162+- : ([ `Append of append t | `Nav of nav t ], string) result =
163163+Ok (`Nav [Mem "valid"])
164164+]}
165165+166166+{1 Evaluation: Navigating JSON}
167167+168168+Now we come to the heart of JSON Pointer: evaluation. RFC 6901, Section 4
169169+describes how a pointer is resolved against a JSON document:
170170+171171+{i Evaluation of a JSON Pointer begins with a reference to the root value
172172+of a JSON document and completes with a reference to some value within
173173+the document. Each reference token in the JSON Pointer is evaluated
174174+sequentially.}
175175+176176+Let's use the example JSON document from RFC 6901, Section 5:
177177+178178+{@ocaml[
179179+# 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}";;
180180+val rfc_example : Jsont.json =
181181+ {"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}
182182+]}
183183+184184+This document is carefully constructed to exercise various edge cases!
185185+186186+{2 The Root Pointer}
187187+188188+{@ocaml[
189189+# get root rfc_example ;;
190190+- : Jsont.json =
191191+{"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}
192192+]}
193193+194194+The empty pointer ({!root}) returns the whole document.
195195+196196+{2 Object Member Access}
197197+198198+{@ocaml[
199199+# get (of_string_nav "/foo") rfc_example ;;
200200+- : Jsont.json = ["bar","baz"]
201201+]}
202202+203203+[/foo] accesses the member named [foo], which is an array.
204204+205205+{2 Array Index Access}
206206+207207+{@ocaml[
208208+# get (of_string_nav "/foo/0") rfc_example ;;
209209+- : Jsont.json = "bar"
210210+# get (of_string_nav "/foo/1") rfc_example ;;
211211+- : Jsont.json = "baz"
212212+]}
213213+214214+[/foo/0] first goes to [foo], then accesses index 0 of the array.
215215+216216+{2 Empty String as Key}
217217+218218+JSON allows empty strings as object keys:
219219+220220+{@ocaml[
221221+# get (of_string_nav "/") rfc_example ;;
222222+- : Jsont.json = 0
223223+]}
224224+225225+The pointer [/] has one token: the empty string. This accesses the member
226226+with an empty name.
227227+228228+{2 Keys with Special Characters}
229229+230230+The RFC example includes keys with [/] and [~] characters:
231231+232232+{@ocaml[
233233+# get (of_string_nav "/a~1b") rfc_example ;;
234234+- : Jsont.json = 1
235235+]}
236236+237237+The token [a~1b] refers to the key [a/b]. We'll explain this escaping
238238+{{:#escaping}below}.
239239+240240+{@ocaml[
241241+# get (of_string_nav "/m~0n") rfc_example ;;
242242+- : Jsont.json = 8
243243+]}
244244+245245+The token [m~0n] refers to the key [m~n].
246246+247247+{b Important}: When using the OCaml library programmatically, you don't need
248248+to worry about escaping. The [Mem] variant holds the literal key name:
249249+250250+{@ocaml[
251251+# let slash_ptr = make [mem "a/b"];;
252252+val slash_ptr : nav t = [Mem "a/b"]
253253+# to_string slash_ptr;;
254254+- : string = "/a~1b"
255255+# get slash_ptr rfc_example ;;
256256+- : Jsont.json = 1
257257+]}
258258+259259+The library escapes it when converting to string.
260260+261261+{2 Other Special Characters (No Escaping Needed)}
262262+263263+Most characters don't need escaping in JSON Pointer strings:
264264+265265+{@ocaml[
266266+# get (of_string_nav "/c%d") rfc_example ;;
267267+- : Jsont.json = 2
268268+# get (of_string_nav "/e^f") rfc_example ;;
269269+- : Jsont.json = 3
270270+# get (of_string_nav "/g|h") rfc_example ;;
271271+- : Jsont.json = 4
272272+# get (of_string_nav "/ ") rfc_example ;;
273273+- : Jsont.json = 7
274274+]}
275275+276276+Even a space is a valid key character!
277277+278278+{2 Error Conditions}
279279+280280+What happens when we try to access something that doesn't exist?
281281+282282+{@ocaml[
283283+# get_result (of_string_nav "/nonexistent") rfc_example;;
284284+- : (Jsont.json, Jsont.Error.t) result =
285285+Error JSON Pointer: member 'nonexistent' not found
286286+File "-":
287287+# find (of_string_nav "/nonexistent") rfc_example;;
288288+- : Jsont.json option = None
289289+]}
290290+291291+Or an out-of-bounds array index:
292292+293293+{@ocaml[
294294+# find (of_string_nav "/foo/99") rfc_example;;
295295+- : Jsont.json option = None
296296+]}
297297+298298+Or try to index into a non-container:
299299+300300+{@ocaml[
301301+# find (of_string_nav "/foo/0/invalid") rfc_example;;
302302+- : Jsont.json option = None
303303+]}
304304+305305+The library provides both exception-raising and result-returning variants:
306306+307307+[
308308+val get : nav t -> Jsont.json -> Jsont.json
309309+val get_result : nav t -> Jsont.json -> (Jsont.json, Jsont.Error.t) result
310310+val find : nav t -> Jsont.json -> Jsont.json option
311311+]
312312+313313+{2 Array Index Rules}
314314+315315+RFC 6901 has specific rules for array indices. Section 4 states:
316316+317317+{i characters comprised of digits [...] that represent an unsigned base-10
318318+integer value, making the new referenced value the array element with
319319+the zero-based index identified by the token}
320320+321321+And importantly:
322322+323323+{i note that leading zeros are not allowed}
324324+325325+{@ocaml[
326326+# of_string_nav "/foo/0";;
327327+- : nav t = [Mem "foo"; Nth 0]
328328+]}
329329+330330+Zero itself is fine.
331331+332332+{@ocaml[
333333+# of_string_nav "/foo/01";;
334334+- : nav t = [Mem "foo"; Mem "01"]
335335+]}
336336+337337+But [01] has a leading zero, so it's NOT treated as an array index - it
338338+becomes a member name instead. This protects against accidental octal
339339+interpretation.
340340+341341+{1 The End-of-Array Marker: [-] and Type Safety}
342342+343343+RFC 6901, Section 4 introduces a special token:
344344+345345+{i exactly the single character "-", making the new referenced value the
346346+(nonexistent) member after the last array element.}
347347+348348+This [-] marker is unique to JSON Pointer (JSON Path has no equivalent).
349349+It's primarily useful for JSON Patch operations (RFC 6902) to append
350350+elements to arrays.
351351+352352+{2 Navigation vs Append Pointers}
353353+354354+The [jsont-pointer] library uses {b phantom types} to encode the difference
355355+between pointers that can be used for navigation and pointers that target
356356+the "append position":
357357+358358+{[
359359+type nav (* A pointer to an existing element *)
360360+type append (* A pointer ending with "-" (append position) *)
361361+type 'a t (* Pointer with phantom type parameter *)
362362+]}
363363+364364+When you parse a pointer, you get either a [nav t] or an [append t]:
365365+366366+{@ocaml[
367367+# of_string "/foo/0";;
368368+- : [ `Append of Jsont_pointer.append Jsont_pointer.t
369369+ | `Nav of Jsont_pointer.nav Jsont_pointer.t ]
370370+= `Nav [Mem "foo"; Nth 0]
371371+# of_string "/foo/-";;
372372+- : [ `Append of Jsont_pointer.append Jsont_pointer.t
373373+ | `Nav of Jsont_pointer.nav Jsont_pointer.t ]
374374+= `Append [Mem "foo"] /-
375375+]}
376376+377377+The [-] creates an [append] pointer. Note that in the internal
378378+representation, the append position is tracked separately (shown as [/-]).
379379+380380+{2 Why Phantom Types?}
381381+382382+The RFC explains that [-] refers to a {e nonexistent} position:
383383+384384+{i Note that the use of the "-" character to index an array will always
385385+result in such an error condition because by definition it refers to
386386+a nonexistent array element.}
387387+388388+So you {b cannot use [get] or [find]} with an append pointer - it makes
389389+no sense to retrieve a value from a position that doesn't exist! The
390390+library enforces this at compile time.
391391+392392+However, append pointers {b are} valid for mutation operations like {!add}:
393393+394394+{x@ocaml[
395395+# let arr_obj = parse_json "{\"foo\":[\"a\",\"b\"]}";;
396396+val arr_obj : Jsont.json = {"foo":["a","b"]}
397397+# (match of_string "/foo/-" with `Append p -> add p arr_obj ~value:(Jsont.Json.string "c") | `Nav _ -> assert false);;
398398+- : Jsont.json = {"foo":["a","b","c"]}
399399+]x}
400400+401401+For convenience, use {!of_string_nav} when you know a pointer shouldn't
402402+contain [-]:
403403+404404+{@ocaml[
405405+# of_string_nav "/foo/0";;
406406+- : Jsont_pointer.nav Jsont_pointer.t = [Mem "foo"; Nth 0]
407407+# of_string_nav "/foo/-";;
408408+Exception:
409409+Jsont.Error Invalid JSON Pointer: '-' not allowed in navigation pointer.
410410+]}
411411+412412+{2 Creating Append Pointers Programmatically}
413413+414414+You can convert a navigation pointer to an append pointer using {!at_end}:
415415+416416+{@ocaml[
417417+# let nav_ptr = of_string_nav "/foo";;
418418+val nav_ptr : Jsont_pointer.nav Jsont_pointer.t = [Mem "foo"]
419419+# let app_ptr = at_end nav_ptr;;
420420+val app_ptr : Jsont_pointer.append Jsont_pointer.t = [Mem "foo"] /-
421421+# to_string app_ptr;;
422422+- : string = "/foo/-"
423423+]}
424424+425425+{1 Mutation Operations}
426426+427427+While RFC 6901 defines JSON Pointer for read-only access, RFC 6902
428428+(JSON Patch) uses JSON Pointer for modifications. The [jsont-pointer]
429429+library provides these operations.
430430+431431+{2 Which Pointer Type for Which Operation?}
432432+433433+The phantom type system enforces correct usage:
434434+435435+{ul
436436+{- {!get}, {!find} - [nav t] only - Can't retrieve from non-existent position}
437437+{- {!remove} - [nav t] only - Can't remove what doesn't exist}
438438+{- {!replace} - [nav t] only - Can't replace what doesn't exist}
439439+{- {!test} - [nav t] only - Can't test non-existent position}
440440+{- {!add} - [_ t] (both) - Can add at existing position OR append}
441441+{- {!set} - [_ t] (both) - Can set existing position OR append}
442442+{- {!move}, {!copy} - [from:nav t], [path:_ t] - Source must exist, dest can be append}
443443+}
444444+445445+{2 Add}
446446+447447+The {!add} operation inserts a value at a location:
448448+449449+{@ocaml[
450450+# let obj = parse_json "{\"foo\":\"bar\"}";;
451451+val obj : Jsont.json = {"foo":"bar"}
452452+# add (of_string_nav "/baz") obj ~value:(Jsont.Json.string "qux")
453453+ ;;
454454+- : Jsont.json = {"foo":"bar","baz":"qux"}
455455+]}
456456+457457+For arrays, {!add} inserts BEFORE the specified index:
458458+459459+{x@ocaml[
460460+# let arr_obj = parse_json "{\"foo\":[\"a\",\"b\"]}";;
461461+val arr_obj : Jsont.json = {"foo":["a","b"]}
462462+# add (of_string_nav "/foo/1") arr_obj ~value:(Jsont.Json.string "X")
463463+ ;;
464464+- : Jsont.json = {"foo":["a","X","b"]}
465465+]x}
466466+467467+This is where the [-] marker and append pointers shine - they append to the end:
468468+469469+{@ocaml[
470470+# (match of_string "/foo/-" with `Append p -> add p arr_obj ~value:(Jsont.Json.string "c") | `Nav _ -> assert false);;
471471+- : Jsont.json = {"foo":["a","b","c"]}]}]}
472472+]}
473473+474474+Or more conveniently using {!at_end}:
475475+476476+{@ocaml[
477477+# add (at_end (of_string_nav "/foo")) arr_obj ~value:(Jsont.Json.string "c")
478478+ ;;
479479+- : Jsont.json = {"foo":["a","b","c"]}]}]}
480480+]}
481481+482482+{2 Remove}
483483+484484+The {!remove} operation deletes a value. It only accepts [nav t] because
485485+you can only remove something that exists:
486486+487487+{@ocaml[
488488+# let two_fields = parse_json "{\"foo\":\"bar\",\"baz\":\"qux\"}";;
489489+val two_fields : Jsont.json = {"foo":"bar","baz":"qux"}
490490+# remove (of_string_nav "/baz") two_fields ;;
491491+- : Jsont.json = {"foo":"bar"}
492492+]}
493493+494494+For arrays, it removes and shifts:
495495+496496+{x@ocaml[
497497+# let three_elem = parse_json "{\"foo\":[\"a\",\"b\",\"c\"]}";;
498498+val three_elem : Jsont.json = {"foo":["a","b","c"]}
499499+# remove (of_string_nav "/foo/1") three_elem ;;
500500+- : Jsont.json = {"foo":["a","c"]}
501501+]x}
502502+503503+{2 Replace}
504504+505505+The {!replace} operation updates an existing value:
506506+507507+{@ocaml[
508508+# replace (of_string_nav "/foo") obj ~value:(Jsont.Json.string "baz")
509509+ ;;
510510+- : Jsont.json = {"foo":"baz"}
511511+]}
512512+513513+Unlike {!add}, {!replace} requires the target to already exist (hence [nav t]).
514514+Attempting to replace a nonexistent path raises an error.
515515+516516+{2 Move}
517517+518518+The {!move} operation relocates a value. The source ([from]) must be a [nav t]
519519+(you can only move something that exists), but the destination ([path]) can
520520+be either:
521521+522522+{@ocaml[
523523+# let nested = parse_json "{\"foo\":{\"bar\":\"baz\"},\"qux\":{}}";;
524524+val nested : Jsont.json = {"foo":{"bar":"baz"},"qux":{}}
525525+# move ~from:(of_string_nav "/foo/bar") ~path:(of_string_nav "/qux/thud") nested
526526+ ;;
527527+- : Jsont.json = {"foo":{},"qux":{"thud":"baz"}}
528528+]}
529529+530530+{2 Copy}
531531+532532+The {!copy} operation duplicates a value (same typing as {!move}):
533533+534534+{@ocaml[
535535+# let to_copy = parse_json "{\"foo\":{\"bar\":\"baz\"}}";;
536536+val to_copy : Jsont.json = {"foo":{"bar":"baz"}}
537537+# copy ~from:(of_string_nav "/foo/bar") ~path:(of_string_nav "/foo/qux") to_copy
538538+ ;;
539539+- : Jsont.json = {"foo":{"bar":"baz","qux":"baz"}}
540540+]}
541541+542542+{2 Test}
543543+544544+The {!test} operation verifies a value (useful in JSON Patch):
545545+546546+{@ocaml[
547547+# test (of_string_nav "/foo") obj ~expected:(Jsont.Json.string "bar");;
548548+- : bool = true
549549+# test (of_string_nav "/foo") obj ~expected:(Jsont.Json.string "wrong");;
550550+- : bool = false
551551+]}
552552+553553+{1:escaping Escaping Special Characters}
554554+555555+RFC 6901, Section 3 explains the escaping rules:
556556+557557+{i Because the characters '~' (%x7E) and '/' (%x2F) have special meanings
558558+in JSON Pointer, '~' needs to be encoded as '~0' and '/' needs to be
559559+encoded as '~1' when these characters appear in a reference token.}
560560+561561+Why these specific characters?
562562+- [/] separates tokens, so it must be escaped inside a token
563563+- [~] is the escape character itself, so it must also be escaped
564564+565565+The escape sequences are:
566566+- [~0] represents [~] (tilde)
567567+- [~1] represents [/] (forward slash)
568568+569569+{2 The Library Handles Escaping Automatically}
570570+571571+{b Important}: When using [jsont-pointer] programmatically, you rarely need
572572+to think about escaping. The [Mem] variant stores unescaped strings,
573573+and escaping happens automatically during serialization:
574574+575575+{@ocaml[
576576+# let p = make [mem "a/b"];;
577577+val p : Jsont_pointer.nav Jsont_pointer.t = [Mem "a/b"]
578578+# to_string p;;
579579+- : string = "/a~1b"
580580+# of_string_nav "/a~1b";;
581581+- : Jsont_pointer.nav Jsont_pointer.t = [Mem "a/b"]
582582+]}
583583+584584+{2 Escaping in Action}
585585+586586+The {!Token} module exposes the escaping functions:
587587+588588+{@ocaml[
589589+# Token.escape "hello";;
590590+- : string = "hello"
591591+# Token.escape "a/b";;
592592+- : string = "a~1b"
593593+# Token.escape "a~b";;
594594+- : string = "a~0b"
595595+# Token.escape "~/";;
596596+- : string = "~0~1"
597597+]}
598598+599599+{2 Unescaping}
600600+601601+And the reverse process:
602602+603603+{@ocaml[
604604+# Token.unescape "a~1b";;
605605+- : string = "a/b"
606606+# Token.unescape "a~0b";;
607607+- : string = "a~b"
608608+]}
609609+610610+{2 The Order Matters!}
611611+612612+RFC 6901, Section 4 is careful to specify the unescaping order:
613613+614614+{i Evaluation of each reference token begins by decoding any escaped
615615+character sequence. This is performed by first transforming any
616616+occurrence of the sequence '~1' to '/', and then transforming any
617617+occurrence of the sequence '~0' to '~'. By performing the substitutions
618618+in this order, an implementation avoids the error of turning '~01' first
619619+into '~1' and then into '/', which would be incorrect (the string '~01'
620620+correctly becomes '~1' after transformation).}
621621+622622+Let's verify this tricky case:
623623+624624+{@ocaml[
625625+# Token.unescape "~01";;
626626+- : string = "~1"
627627+]}
628628+629629+If we unescaped [~0] first, [~01] would become [~1], which would then become
630630+[/]. But that's wrong! The sequence [~01] should become the literal string
631631+[~1] (a tilde followed by the digit one).
632632+633633+{1 URI Fragment Encoding}
634634+635635+JSON Pointers can be embedded in URIs. RFC 6901, Section 6 explains:
636636+637637+{i A JSON Pointer can be represented in a URI fragment identifier by
638638+encoding it into octets using UTF-8, while percent-encoding those
639639+characters not allowed by the fragment rule in RFC 3986.}
640640+641641+This adds percent-encoding on top of the [~0]/[~1] escaping:
642642+643643+{@ocaml[
644644+# to_uri_fragment (of_string_nav "/foo");;
645645+- : string = "/foo"
646646+# to_uri_fragment (of_string_nav "/a~1b");;
647647+- : string = "/a~1b"
648648+# to_uri_fragment (of_string_nav "/c%d");;
649649+- : string = "/c%25d"
650650+# to_uri_fragment (of_string_nav "/ ");;
651651+- : string = "/%20"
652652+]}
653653+654654+The [%] character must be percent-encoded as [%25] in URIs, and
655655+spaces become [%20].
656656+657657+Here's the RFC example showing the URI fragment forms:
658658+659659+{ul
660660+{- [""] → [#] → whole document}
661661+{- ["/foo"] → [#/foo] → [["bar", "baz"]]}
662662+{- ["/foo/0"] → [#/foo/0] → ["bar"]}
663663+{- ["/"] → [#/] → [0]}
664664+{- ["/a~1b"] → [#/a~1b] → [1]}
665665+{- ["/c%d"] → [#/c%25d] → [2]}
666666+{- ["/ "] → [#/%20] → [7]}
667667+{- ["/m~0n"] → [#/m~0n] → [8]}
668668+}
669669+670670+{1 Building Pointers Programmatically}
671671+672672+Instead of parsing strings, you can build pointers from indices:
673673+674674+{@ocaml[
675675+# let port_ptr = make [mem "database"; mem "port"];;
676676+val port_ptr : Jsont_pointer.nav Jsont_pointer.t =
677677+ [Mem "database"; Mem "port"]
678678+# to_string port_ptr;;
679679+- : string = "/database/port"
680680+]}
681681+682682+For array access, use the {!nth} helper:
683683+684684+{@ocaml[
685685+# let first_feature_ptr = make [mem "features"; nth 0];;
686686+val first_feature_ptr : Jsont_pointer.nav Jsont_pointer.t =
687687+ [Mem "features"; Nth 0]
688688+# to_string first_feature_ptr;;
689689+- : string = "/features/0"
690690+]}
691691+692692+{2 Pointer Navigation}
693693+694694+You can build pointers incrementally using the [/] operator (or {!append_index}):
695695+696696+{@ocaml[
697697+# let db_ptr = of_string_nav "/database";;
698698+val db_ptr : Jsont_pointer.nav Jsont_pointer.t = [Mem "database"]
699699+# let creds_ptr = db_ptr / mem "credentials";;
700700+val creds_ptr : Jsont_pointer.nav Jsont_pointer.t =
701701+ [Mem "database"; Mem "credentials"]
702702+# let user_ptr = creds_ptr / mem "username";;
703703+val user_ptr : Jsont_pointer.nav Jsont_pointer.t =
704704+ [Mem "database"; Mem "credentials"; Mem "username"]
705705+# to_string user_ptr;;
706706+- : string = "/database/credentials/username"
707707+]}
708708+709709+Or concatenate two pointers:
710710+711711+{@ocaml[
712712+# let base = of_string_nav "/api/v1";;
713713+val base : Jsont_pointer.nav Jsont_pointer.t = [Mem "api"; Mem "v1"]
714714+# let endpoint = of_string_nav "/users/0";;
715715+val endpoint : Jsont_pointer.nav Jsont_pointer.t = [Mem "users"; Nth 0]
716716+# to_string (concat base endpoint);;
717717+- : string = "/api/v1/users/0"
718718+]}
719719+720720+{1 Jsont Integration}
721721+722722+The library integrates with the {!Jsont} codec system, allowing you to
723723+combine JSON Pointer navigation with typed decoding. This is powerful
724724+because you can point to a location in a JSON document and decode it
725725+directly to an OCaml type.
726726+727727+{x@ocaml[
728728+# let config_json = parse_json "{\"database\":{\"host\":\"localhost\",\"port\":5432,\"credentials\":{\"username\":\"admin\",\"password\":\"secret\"}},\"features\":[\"auth\",\"logging\",\"metrics\"]}";;
729729+val config_json : Jsont.json =
730730+ {"database":{"host":"localhost","port":5432,"credentials":{"username":"admin","password":"secret"}},"features":["auth","logging","metrics"]}
731731+]x}
732732+733733+{2 Typed Access with [path]}
734734+735735+The {!path} combinator combines pointer navigation with typed decoding:
736736+737737+{@ocaml[
738738+# let db_host =
739739+ Jsont.Json.decode
740740+ (path (of_string_nav "/database/host") Jsont.string)
741741+ config_json
742742+ |> Result.get_ok;;
743743+val db_host : string = "localhost"
744744+# let db_port =
745745+ Jsont.Json.decode
746746+ (path (of_string_nav "/database/port") Jsont.int)
747747+ config_json
748748+ |> Result.get_ok;;
749749+val db_port : int = 5432
750750+]}
751751+752752+Extract a list of strings:
753753+754754+{@ocaml[
755755+# let features =
756756+ Jsont.Json.decode
757757+ (path (of_string_nav "/features") Jsont.(list string))
758758+ config_json
759759+ |> Result.get_ok;;
760760+val features : string list = ["auth"; "logging"; "metrics"]
761761+]}
762762+763763+{2 Default Values with [~absent]}
764764+765765+Use [~absent] to provide a default when a path doesn't exist:
766766+767767+{@ocaml[
768768+# let timeout =
769769+ Jsont.Json.decode
770770+ (path ~absent:30 (of_string_nav "/database/timeout") Jsont.int)
771771+ config_json
772772+ |> Result.get_ok;;
773773+val timeout : int = 30
774774+]}
775775+776776+{2 Nested Path Extraction}
777777+778778+You can extract values from deeply nested structures:
779779+780780+{x@ocaml[
781781+# let org_json = parse_json "{\"organization\":{\"owner\":{\"name\":\"Alice\",\"email\":\"alice@example.com\",\"age\":35},\"members\":[{\"name\":\"Bob\",\"email\":\"bob@example.com\",\"age\":28}]}}";;
782782+val org_json : Jsont.json =
783783+ {"organization":{"owner":{"name":"Alice","email":"alice@example.com","age":35},"members":[{"name":"Bob","email":"bob@example.com","age":28}]}}
784784+# Jsont.Json.decode
785785+ (path (of_string_nav "/organization/owner/name") Jsont.string)
786786+ org_json
787787+ |> Result.get_ok;;
788788+- : string = "Alice"
789789+# Jsont.Json.decode
790790+ (path (of_string_nav "/organization/members/0/age") Jsont.int)
791791+ org_json
792792+ |> Result.get_ok;;
793793+- : int = 28
794794+]x}
795795+796796+{2 Comparison: Raw vs Typed Access}
797797+798798+{b Raw access} requires pattern matching:
799799+800800+{@ocaml[
801801+# let raw_port =
802802+ match get (of_string_nav "/database/port") config_json with
803803+ | Jsont.Number (f, _) -> int_of_float f
804804+ | _ -> failwith "expected number";;
805805+val raw_port : int = 5432
806806+]}
807807+808808+{b Typed access} is cleaner and type-safe:
809809+810810+{@ocaml[
811811+# let typed_port =
812812+ Jsont.Json.decode
813813+ (path (of_string_nav "/database/port") Jsont.int)
814814+ config_json
815815+ |> Result.get_ok;;
816816+val typed_port : int = 5432
817817+]}
818818+819819+The typed approach catches mismatches at decode time with clear errors.
820820+821821+{1 Summary}
822822+823823+JSON Pointer (RFC 6901) provides a simple but powerful way to address
824824+values within JSON documents:
825825+826826+{ol
827827+{- {b Syntax}: Pointers are strings of [/]-separated reference tokens}
828828+{- {b Escaping}: Use [~0] for [~] and [~1] for [/] in tokens (handled automatically by the library)}
829829+{- {b Evaluation}: Tokens navigate through objects (by key) and arrays (by index)}
830830+{- {b URI Encoding}: Pointers can be percent-encoded for use in URIs}
831831+{- {b Mutations}: Combined with JSON Patch (RFC 6902), pointers enable structured updates}
832832+{- {b Type Safety}: Phantom types ([nav t] vs [append t]) prevent misuse of append pointers with retrieval operations}
833833+}
834834+835835+The [jsont-pointer] library implements all of this with type-safe OCaml
836836+interfaces, integration with the [jsont] codec system, and proper error
837837+handling for malformed pointers and missing values.
838838+839839+{2 Key Points on JSON Pointer vs JSON Path}
840840+841841+{ul
842842+{- {b JSON Pointer} addresses a {e single} location (like a file path)}
843843+{- {b JSON Path} queries for {e multiple} values (like a search)}
844844+{- The [-] token is unique to JSON Pointer - it means "append position" for arrays}
845845+{- The library uses phantom types to enforce that [-] (append) pointers cannot be used with [get]/[find]}
846846+}
+18-9
src/top/jsont_pointer_top.ml
···2828 "Jsont_pointer_top.json_printer";
2929 "Jsont_pointer_top.error_printer" ]
30303131-let eval_string
3232- ?(print_outcome = false) ?(err_formatter = Format.err_formatter) str =
3333- let lexbuf = Lexing.from_string str in
3434- let phrase = !Toploop.parse_toplevel_phrase lexbuf in
3535- Toploop.execute_phrase print_outcome err_formatter phrase
3131+(* Suppress stderr during printer installation to avoid noise in MDX tests *)
3232+let null_formatter = Format.make_formatter (fun _ _ _ -> ()) (fun () -> ())
3333+3434+let eval_string_quiet str =
3535+ try
3636+ let lexbuf = Lexing.from_string str in
3737+ let phrase = !Toploop.parse_toplevel_phrase lexbuf in
3838+ Toploop.execute_phrase false null_formatter phrase
3939+ with _ -> false
36403737-let rec install_printers = function
4141+let rec do_install_printers = function
3842 | [] -> true
3943 | printer :: rest ->
4044 let cmd = Printf.sprintf "#install_printer %s;;" printer in
4141- eval_string cmd && install_printers rest
4545+ eval_string_quiet cmd && do_install_printers rest
4646+4747+let install () =
4848+ (* Silently ignore failures - this handles non-toplevel contexts like MDX *)
4949+ ignore (do_install_printers printers)
42505151+(* Only auto-install when OCAML_TOPLEVEL_NAME is set, indicating a real toplevel *)
4352let () =
4444- if not (install_printers printers) then
4545- Format.eprintf "Problem installing jsont-pointer printers@."
5353+ if Sys.getenv_opt "OCAML_TOPLEVEL_NAME" <> None then
5454+ install ()
+5
src/top/jsont_pointer_top.mli
···3838val error_printer : Format.formatter -> Jsont.Error.t -> unit
3939(** [error_printer] formats a {!Jsont.Error.t} as a human-readable
4040 error message. Suitable for use with [#install_printer]. *)
4141+4242+val install : unit -> unit
4343+(** [install ()] installs all printers. This is called automatically when
4444+ the library is loaded, but can be called again if needed (e.g., in
4545+ test environments where automatic initialization doesn't run). *)