+5
bin/dune
+5
bin/dune
+205
bin/jsonpp.ml
+205
bin/jsonpp.ml
···
1
+
(* Test runner for jsont_pointer *)
2
+
3
+
let read_file path =
4
+
let ic = open_in path in
5
+
let n = in_channel_length ic in
6
+
let s = really_input_string ic n in
7
+
close_in ic;
8
+
s
9
+
10
+
let parse_json s =
11
+
match Jsont_bytesrw.decode_string Jsont.json s with
12
+
| Ok json -> json
13
+
| Error e -> failwith e
14
+
15
+
let json_to_string json =
16
+
match Jsont_bytesrw.encode_string Jsont.json json with
17
+
| Ok s -> s
18
+
| Error e -> failwith e
19
+
20
+
(* Test: parse pointer and print indices *)
21
+
let test_parse pointer_str =
22
+
try
23
+
let p = Jsont_pointer.of_string pointer_str in
24
+
let indices = Jsont_pointer.indices p in
25
+
let index_strs = List.map (fun idx ->
26
+
match idx with
27
+
| Jsont_pointer.Index.Mem s -> Printf.sprintf "Mem:%s" s
28
+
| Jsont_pointer.Index.Nth n -> Printf.sprintf "Nth:%d" n
29
+
| Jsont_pointer.Index.End -> "End"
30
+
) indices in
31
+
Printf.printf "OK: [%s]\n" (String.concat ", " index_strs)
32
+
with Jsont.Error e ->
33
+
Printf.printf "ERROR: %s\n" (Jsont.Error.to_string e)
34
+
35
+
(* Test: roundtrip pointer string *)
36
+
let test_roundtrip pointer_str =
37
+
try
38
+
let p = Jsont_pointer.of_string pointer_str in
39
+
let s = Jsont_pointer.to_string p in
40
+
if s = pointer_str then
41
+
Printf.printf "OK: %s\n" s
42
+
else
43
+
Printf.printf "MISMATCH: input=%s output=%s\n" pointer_str s
44
+
with Jsont.Error e ->
45
+
Printf.printf "ERROR: %s\n" (Jsont.Error.to_string e)
46
+
47
+
(* Test: evaluate pointer against JSON *)
48
+
let test_eval json_path pointer_str =
49
+
try
50
+
let json = parse_json (read_file json_path) in
51
+
let p = Jsont_pointer.of_string pointer_str in
52
+
let result = Jsont_pointer.get p json in
53
+
Printf.printf "OK: %s\n" (json_to_string result)
54
+
with
55
+
| Jsont.Error e ->
56
+
Printf.printf "ERROR: %s\n" (Jsont.Error.to_string e)
57
+
| Failure e ->
58
+
Printf.printf "FAIL: %s\n" e
59
+
60
+
(* Test: escape token *)
61
+
let test_escape token =
62
+
let escaped = Jsont_pointer.Token.escape token in
63
+
Printf.printf "%s\n" escaped
64
+
65
+
(* Test: unescape token *)
66
+
let test_unescape token =
67
+
try
68
+
let unescaped = Jsont_pointer.Token.unescape token in
69
+
Printf.printf "OK: %s\n" unescaped
70
+
with Jsont.Error e ->
71
+
Printf.printf "ERROR: %s\n" (Jsont.Error.to_string e)
72
+
73
+
(* Test: URI fragment roundtrip *)
74
+
let test_uri_fragment pointer_str =
75
+
try
76
+
let p = Jsont_pointer.of_string pointer_str in
77
+
let frag = Jsont_pointer.to_uri_fragment p in
78
+
let p2 = Jsont_pointer.of_uri_fragment frag in
79
+
let s2 = Jsont_pointer.to_string p2 in
80
+
if s2 = pointer_str then
81
+
Printf.printf "OK: %s -> %s\n" pointer_str frag
82
+
else
83
+
Printf.printf "MISMATCH: %s -> %s -> %s\n" pointer_str frag s2
84
+
with Jsont.Error e ->
85
+
Printf.printf "ERROR: %s\n" (Jsont.Error.to_string e)
86
+
87
+
(* Test: add operation *)
88
+
let test_add json_str pointer_str value_str =
89
+
try
90
+
let json = parse_json json_str in
91
+
let p = Jsont_pointer.of_string pointer_str in
92
+
let value = parse_json value_str in
93
+
let result = Jsont_pointer.add p json ~value in
94
+
Printf.printf "%s\n" (json_to_string result)
95
+
with Jsont.Error e ->
96
+
Printf.printf "ERROR: %s\n" (Jsont.Error.to_string e)
97
+
98
+
(* Test: remove operation *)
99
+
let test_remove json_str pointer_str =
100
+
try
101
+
let json = parse_json json_str in
102
+
let p = Jsont_pointer.of_string pointer_str in
103
+
let result = Jsont_pointer.remove p json in
104
+
Printf.printf "%s\n" (json_to_string result)
105
+
with Jsont.Error e ->
106
+
Printf.printf "ERROR: %s\n" (Jsont.Error.to_string e)
107
+
108
+
(* Test: replace operation *)
109
+
let test_replace json_str pointer_str value_str =
110
+
try
111
+
let json = parse_json json_str in
112
+
let p = Jsont_pointer.of_string pointer_str in
113
+
let value = parse_json value_str in
114
+
let result = Jsont_pointer.replace p json ~value in
115
+
Printf.printf "%s\n" (json_to_string result)
116
+
with Jsont.Error e ->
117
+
Printf.printf "ERROR: %s\n" (Jsont.Error.to_string e)
118
+
119
+
(* Test: move operation *)
120
+
let test_move json_str from_str path_str =
121
+
try
122
+
let json = parse_json json_str in
123
+
let from = Jsont_pointer.of_string from_str in
124
+
let path = Jsont_pointer.of_string path_str in
125
+
let result = Jsont_pointer.move ~from ~path json in
126
+
Printf.printf "%s\n" (json_to_string result)
127
+
with Jsont.Error e ->
128
+
Printf.printf "ERROR: %s\n" (Jsont.Error.to_string e)
129
+
130
+
(* Test: copy operation *)
131
+
let test_copy json_str from_str path_str =
132
+
try
133
+
let json = parse_json json_str in
134
+
let from = Jsont_pointer.of_string from_str in
135
+
let path = Jsont_pointer.of_string path_str in
136
+
let result = Jsont_pointer.copy ~from ~path json in
137
+
Printf.printf "%s\n" (json_to_string result)
138
+
with Jsont.Error e ->
139
+
Printf.printf "ERROR: %s\n" (Jsont.Error.to_string e)
140
+
141
+
(* Test: test operation *)
142
+
let test_test json_str pointer_str expected_str =
143
+
try
144
+
let json = parse_json json_str in
145
+
let p = Jsont_pointer.of_string pointer_str in
146
+
let expected = parse_json expected_str in
147
+
let result = Jsont_pointer.test p json ~expected in
148
+
Printf.printf "%b\n" result
149
+
with Jsont.Error e ->
150
+
Printf.printf "ERROR: %s\n" (Jsont.Error.to_string e)
151
+
152
+
(* Test: has operation (checks if pointer exists) *)
153
+
let test_has json_str pointer_str =
154
+
try
155
+
let json = parse_json json_str in
156
+
let p = Jsont_pointer.of_string pointer_str in
157
+
let result = Jsont_pointer.find p json in
158
+
Printf.printf "%b\n" (Option.is_some result)
159
+
with Jsont.Error e ->
160
+
Printf.printf "ERROR: %s\n" (Jsont.Error.to_string e)
161
+
162
+
let () =
163
+
match Array.to_list Sys.argv with
164
+
| _ :: "parse" :: pointer :: _ ->
165
+
test_parse pointer
166
+
| _ :: "roundtrip" :: pointer :: _ ->
167
+
test_roundtrip pointer
168
+
| _ :: "eval" :: json_path :: pointer :: _ ->
169
+
test_eval json_path pointer
170
+
| _ :: "escape" :: token :: _ ->
171
+
test_escape token
172
+
| _ :: "unescape" :: token :: _ ->
173
+
test_unescape token
174
+
| _ :: "uri-fragment" :: pointer :: _ ->
175
+
test_uri_fragment pointer
176
+
| _ :: "add" :: json :: pointer :: value :: _ ->
177
+
test_add json pointer value
178
+
| _ :: "remove" :: json :: pointer :: _ ->
179
+
test_remove json pointer
180
+
| _ :: "replace" :: json :: pointer :: value :: _ ->
181
+
test_replace json pointer value
182
+
| _ :: "move" :: json :: from :: path :: _ ->
183
+
test_move json from path
184
+
| _ :: "copy" :: json :: from :: path :: _ ->
185
+
test_copy json from path
186
+
| _ :: "test" :: json :: pointer :: expected :: _ ->
187
+
test_test json pointer expected
188
+
| _ :: "has" :: json :: pointer :: _ ->
189
+
test_has json pointer
190
+
| _ ->
191
+
Printf.printf "Usage:\n";
192
+
Printf.printf " test_pointer parse <pointer>\n";
193
+
Printf.printf " test_pointer roundtrip <pointer>\n";
194
+
Printf.printf " test_pointer eval <json-file> <pointer>\n";
195
+
Printf.printf " test_pointer escape <token>\n";
196
+
Printf.printf " test_pointer unescape <token>\n";
197
+
Printf.printf " test_pointer uri-fragment <pointer>\n";
198
+
Printf.printf " test_pointer add <json> <pointer> <value>\n";
199
+
Printf.printf " test_pointer remove <json> <pointer>\n";
200
+
Printf.printf " test_pointer replace <json> <pointer> <value>\n";
201
+
Printf.printf " test_pointer move <json> <from> <path>\n";
202
+
Printf.printf " test_pointer copy <json> <from> <path>\n";
203
+
Printf.printf " test_pointer test <json> <pointer> <expected>\n";
204
+
Printf.printf " test_pointer has <json> <pointer>\n";
205
+
exit 1
+12
doc/rfc6901_example.json
+12
doc/rfc6901_example.json
+577
doc/tutorial.md
+577
doc/tutorial.md
···
1
+
# JSON Pointer Tutorial
2
+
3
+
This tutorial introduces JSON Pointer as defined in
4
+
[RFC 6901](https://www.rfc-editor.org/rfc/rfc6901), and demonstrates
5
+
the `jsont-pointer` OCaml library through interactive examples.
6
+
7
+
## What is JSON Pointer?
8
+
9
+
From RFC 6901, Section 1:
10
+
11
+
> JSON Pointer defines a string syntax for identifying a specific value
12
+
> within a JavaScript Object Notation (JSON) document.
13
+
14
+
In other words, JSON Pointer is an addressing scheme for locating values
15
+
inside a JSON structure. Think of it like a filesystem path, but for JSON
16
+
documents instead of files.
17
+
18
+
For example, given this JSON document:
19
+
20
+
```json
21
+
{
22
+
"users": [
23
+
{"name": "Alice", "age": 30},
24
+
{"name": "Bob", "age": 25}
25
+
]
26
+
}
27
+
```
28
+
29
+
The JSON Pointer `/users/0/name` refers to the string `"Alice"`.
30
+
31
+
## Syntax: Reference Tokens
32
+
33
+
RFC 6901, Section 3 defines the syntax:
34
+
35
+
> A JSON Pointer is a Unicode string containing a sequence of zero or more
36
+
> reference tokens, each prefixed by a '/' (%x2F) character.
37
+
38
+
The grammar is elegantly simple:
39
+
40
+
```
41
+
json-pointer = *( "/" reference-token )
42
+
reference-token = *( unescaped / escaped )
43
+
```
44
+
45
+
This means:
46
+
- The empty string `""` is a valid pointer (it refers to the whole document)
47
+
- Every non-empty pointer starts with `/`
48
+
- Everything between `/` characters is a "reference token"
49
+
50
+
Let's see this in action. We can parse pointers and see their structure:
51
+
52
+
```sh
53
+
$ jsonpp parse ""
54
+
OK: []
55
+
```
56
+
57
+
The empty pointer has no reference tokens - it points to the root.
58
+
59
+
```sh
60
+
$ jsonpp parse "/foo"
61
+
OK: [Mem:foo]
62
+
```
63
+
64
+
The pointer `/foo` has one token: `foo`. Since it's not a number, it's
65
+
interpreted as an object member name (`Mem`).
66
+
67
+
```sh
68
+
$ jsonpp parse "/foo/0"
69
+
OK: [Mem:foo, Nth:0]
70
+
```
71
+
72
+
Here we have two tokens: `foo` (a member name) and `0` (interpreted as
73
+
an array index `Nth`).
74
+
75
+
```sh
76
+
$ jsonpp parse "/foo/bar/baz"
77
+
OK: [Mem:foo, Mem:bar, Mem:baz]
78
+
```
79
+
80
+
Multiple tokens navigate deeper into nested structures.
81
+
82
+
### Invalid Syntax
83
+
84
+
What happens if a pointer doesn't start with `/`?
85
+
86
+
```sh
87
+
$ jsonpp parse "foo"
88
+
ERROR: Invalid JSON Pointer: must be empty or start with '/': foo
89
+
```
90
+
91
+
The RFC is strict: non-empty pointers MUST start with `/`.
92
+
93
+
## Escaping Special Characters
94
+
95
+
RFC 6901, Section 3 explains the escaping rules:
96
+
97
+
> Because the characters '~' (%x7E) and '/' (%x2F) have special meanings
98
+
> in JSON Pointer, '~' needs to be encoded as '~0' and '/' needs to be
99
+
> encoded as '~1' when these characters appear in a reference token.
100
+
101
+
Why these specific characters?
102
+
- `/` separates tokens, so it must be escaped inside a token
103
+
- `~` is the escape character itself, so it must also be escaped
104
+
105
+
The escape sequences are:
106
+
- `~0` represents `~` (tilde)
107
+
- `~1` represents `/` (forward slash)
108
+
109
+
Let's see escaping in action:
110
+
111
+
```sh
112
+
$ jsonpp escape "hello"
113
+
hello
114
+
```
115
+
116
+
No special characters, no escaping needed.
117
+
118
+
```sh
119
+
$ jsonpp escape "a/b"
120
+
a~1b
121
+
```
122
+
123
+
The `/` becomes `~1`.
124
+
125
+
```sh
126
+
$ jsonpp escape "a~b"
127
+
a~0b
128
+
```
129
+
130
+
The `~` becomes `~0`.
131
+
132
+
```sh
133
+
$ jsonpp escape "~/"
134
+
~0~1
135
+
```
136
+
137
+
Both characters are escaped.
138
+
139
+
### Unescaping
140
+
141
+
And the reverse process:
142
+
143
+
```sh
144
+
$ jsonpp unescape "a~1b"
145
+
OK: a/b
146
+
```
147
+
148
+
```sh
149
+
$ jsonpp unescape "a~0b"
150
+
OK: a~b
151
+
```
152
+
153
+
### The Order Matters!
154
+
155
+
RFC 6901, Section 4 is careful to specify the unescaping order:
156
+
157
+
> Evaluation of each reference token begins by decoding any escaped
158
+
> character sequence. This is performed by first transforming any
159
+
> occurrence of the sequence '~1' to '/', and then transforming any
160
+
> occurrence of the sequence '~0' to '~'. By performing the substitutions
161
+
> in this order, an implementation avoids the error of turning '~01' first
162
+
> into '~1' and then into '/', which would be incorrect (the string '~01'
163
+
> correctly becomes '~1' after transformation).
164
+
165
+
Let's verify this tricky case:
166
+
167
+
```sh
168
+
$ jsonpp unescape "~01"
169
+
OK: ~1
170
+
```
171
+
172
+
If we unescaped `~0` first, `~01` would become `~1`, which would then become
173
+
`/`. But that's wrong! The sequence `~01` should become the literal string
174
+
`~1` (a tilde followed by the digit one).
175
+
176
+
Invalid escape sequences are rejected:
177
+
178
+
```sh
179
+
$ jsonpp unescape "~2"
180
+
ERROR: Invalid JSON Pointer: invalid escape sequence ~2
181
+
```
182
+
183
+
```sh
184
+
$ jsonpp unescape "hello~"
185
+
ERROR: Invalid JSON Pointer: incomplete escape sequence at end
186
+
```
187
+
188
+
## Evaluation: Navigating JSON
189
+
190
+
Now we come to the heart of JSON Pointer: evaluation. RFC 6901, Section 4
191
+
describes how a pointer is resolved against a JSON document:
192
+
193
+
> Evaluation of a JSON Pointer begins with a reference to the root value
194
+
> of a JSON document and completes with a reference to some value within
195
+
> the document. Each reference token in the JSON Pointer is evaluated
196
+
> sequentially.
197
+
198
+
Let's use the example JSON document from RFC 6901, Section 5:
199
+
200
+
```sh
201
+
$ cat rfc6901_example.json
202
+
{
203
+
"foo": ["bar", "baz"],
204
+
"": 0,
205
+
"a/b": 1,
206
+
"c%d": 2,
207
+
"e^f": 3,
208
+
"g|h": 4,
209
+
"i\\j": 5,
210
+
"k\"l": 6,
211
+
" ": 7,
212
+
"m~n": 8
213
+
}
214
+
```
215
+
216
+
This document is carefully constructed to exercise various edge cases!
217
+
218
+
### The Root Pointer
219
+
220
+
```sh
221
+
$ jsonpp eval rfc6901_example.json ""
222
+
OK: {"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}
223
+
```
224
+
225
+
The empty pointer returns the whole document.
226
+
227
+
### Object Member Access
228
+
229
+
```sh
230
+
$ jsonpp eval rfc6901_example.json "/foo"
231
+
OK: ["bar","baz"]
232
+
```
233
+
234
+
`/foo` accesses the member named `foo`, which is an array.
235
+
236
+
### Array Index Access
237
+
238
+
```sh
239
+
$ jsonpp eval rfc6901_example.json "/foo/0"
240
+
OK: "bar"
241
+
```
242
+
243
+
`/foo/0` first goes to `foo`, then accesses index 0 of the array.
244
+
245
+
```sh
246
+
$ jsonpp eval rfc6901_example.json "/foo/1"
247
+
OK: "baz"
248
+
```
249
+
250
+
Index 1 gives us the second element.
251
+
252
+
### Empty String as Key
253
+
254
+
JSON allows empty strings as object keys:
255
+
256
+
```sh
257
+
$ jsonpp eval rfc6901_example.json "/"
258
+
OK: 0
259
+
```
260
+
261
+
The pointer `/` has one token: the empty string. This accesses the member
262
+
with an empty name.
263
+
264
+
### Keys with Special Characters
265
+
266
+
Now for the escape sequences:
267
+
268
+
```sh
269
+
$ jsonpp eval rfc6901_example.json "/a~1b"
270
+
OK: 1
271
+
```
272
+
273
+
The token `a~1b` unescapes to `a/b`, which is the key name.
274
+
275
+
```sh
276
+
$ jsonpp eval rfc6901_example.json "/m~0n"
277
+
OK: 8
278
+
```
279
+
280
+
The token `m~0n` unescapes to `m~n`.
281
+
282
+
### Other Special Characters (No Escaping Needed)
283
+
284
+
Most characters don't need escaping in JSON Pointer strings:
285
+
286
+
```sh
287
+
$ jsonpp eval rfc6901_example.json "/c%d"
288
+
OK: 2
289
+
```
290
+
291
+
```sh
292
+
$ jsonpp eval rfc6901_example.json "/e^f"
293
+
OK: 3
294
+
```
295
+
296
+
```sh
297
+
$ jsonpp eval rfc6901_example.json "/g|h"
298
+
OK: 4
299
+
```
300
+
301
+
```sh
302
+
$ jsonpp eval rfc6901_example.json "/ "
303
+
OK: 7
304
+
```
305
+
306
+
Even a space is a valid key character!
307
+
308
+
### Error Conditions
309
+
310
+
What happens when we try to access something that doesn't exist?
311
+
312
+
```sh
313
+
$ jsonpp eval rfc6901_example.json "/nonexistent"
314
+
ERROR: JSON Pointer: member 'nonexistent' not found
315
+
File "-":
316
+
```
317
+
318
+
Or an out-of-bounds array index:
319
+
320
+
```sh
321
+
$ jsonpp eval rfc6901_example.json "/foo/99"
322
+
ERROR: JSON Pointer: index 99 out of bounds (array has 2 elements)
323
+
File "-":
324
+
```
325
+
326
+
Or try to index into a non-container:
327
+
328
+
```sh
329
+
$ jsonpp eval rfc6901_example.json "/foo/0/invalid"
330
+
ERROR: JSON Pointer: cannot index into string with 'invalid'
331
+
File "-":
332
+
```
333
+
334
+
### Array Index Rules
335
+
336
+
RFC 6901 has specific rules for array indices. Section 4 states:
337
+
338
+
> characters comprised of digits [...] that represent an unsigned base-10
339
+
> integer value, making the new referenced value the array element with
340
+
> the zero-based index identified by the token
341
+
342
+
And importantly:
343
+
344
+
> note that leading zeros are not allowed
345
+
346
+
```sh
347
+
$ jsonpp parse "/foo/0"
348
+
OK: [Mem:foo, Nth:0]
349
+
```
350
+
351
+
Zero itself is fine.
352
+
353
+
```sh
354
+
$ jsonpp parse "/foo/01"
355
+
OK: [Mem:foo, Mem:01]
356
+
```
357
+
358
+
But `01` has a leading zero, so it's NOT treated as an array index - it
359
+
becomes a member name instead. This protects against accidental octal
360
+
interpretation.
361
+
362
+
## The End-of-Array Marker: `-`
363
+
364
+
RFC 6901, Section 4 introduces a special token:
365
+
366
+
> exactly the single character "-", making the new referenced value the
367
+
> (nonexistent) member after the last array element.
368
+
369
+
This is primarily useful for JSON Patch operations (RFC 6902). Let's see
370
+
how it parses:
371
+
372
+
```sh
373
+
$ jsonpp parse "/foo/-"
374
+
OK: [Mem:foo, End]
375
+
```
376
+
377
+
The `-` is recognized as a special `End` index.
378
+
379
+
However, you cannot evaluate a pointer containing `-` because it refers
380
+
to a position that doesn't exist:
381
+
382
+
```sh
383
+
$ jsonpp eval rfc6901_example.json "/foo/-"
384
+
ERROR: JSON Pointer: '-' (end marker) refers to nonexistent array element
385
+
File "-":
386
+
```
387
+
388
+
The RFC explains this:
389
+
390
+
> Note that the use of the "-" character to index an array will always
391
+
> result in such an error condition because by definition it refers to
392
+
> a nonexistent array element.
393
+
394
+
But we'll see later that `-` is very useful for mutation operations!
395
+
396
+
## URI Fragment Encoding
397
+
398
+
JSON Pointers can be embedded in URIs. RFC 6901, Section 6 explains:
399
+
400
+
> A JSON Pointer can be represented in a URI fragment identifier by
401
+
> encoding it into octets using UTF-8, while percent-encoding those
402
+
> characters not allowed by the fragment rule in RFC 3986.
403
+
404
+
This adds percent-encoding on top of the `~0`/`~1` escaping:
405
+
406
+
```sh
407
+
$ jsonpp uri-fragment "/foo"
408
+
OK: /foo -> /foo
409
+
```
410
+
411
+
Simple pointers often don't need percent-encoding.
412
+
413
+
```sh
414
+
$ jsonpp uri-fragment "/a~1b"
415
+
OK: /a~1b -> /a~1b
416
+
```
417
+
418
+
The `~1` escape stays as-is (it's valid in URI fragments).
419
+
420
+
```sh
421
+
$ jsonpp uri-fragment "/c%d"
422
+
OK: /c%d -> /c%25d
423
+
```
424
+
425
+
The `%` character must be percent-encoded as `%25` in URIs!
426
+
427
+
```sh
428
+
$ jsonpp uri-fragment "/ "
429
+
OK: / -> /%20
430
+
```
431
+
432
+
Spaces become `%20`.
433
+
434
+
Here's the RFC example showing the URI fragment forms:
435
+
436
+
| JSON Pointer | URI Fragment | Value |
437
+
|-------------|-------------|-------|
438
+
| `""` | `#` | whole document |
439
+
| `"/foo"` | `#/foo` | `["bar", "baz"]` |
440
+
| `"/foo/0"` | `#/foo/0` | `"bar"` |
441
+
| `"/"` | `#/` | `0` |
442
+
| `"/a~1b"` | `#/a~1b` | `1` |
443
+
| `"/c%d"` | `#/c%25d` | `2` |
444
+
| `"/ "` | `#/%20` | `7` |
445
+
| `"/m~0n"` | `#/m~0n` | `8` |
446
+
447
+
## Mutation Operations
448
+
449
+
While RFC 6901 defines JSON Pointer for read-only access, RFC 6902
450
+
(JSON Patch) uses JSON Pointer for modifications. The `jsont-pointer`
451
+
library provides these operations.
452
+
453
+
### Add
454
+
455
+
The `add` operation inserts a value at a location:
456
+
457
+
```sh
458
+
$ jsonpp add '{"foo":"bar"}' '/baz' '"qux"'
459
+
{"foo":"bar","baz":"qux"}
460
+
```
461
+
462
+
For arrays, `add` inserts BEFORE the specified index:
463
+
464
+
```sh
465
+
$ jsonpp add '{"foo":["a","b"]}' '/foo/1' '"X"'
466
+
{"foo":["a","X","b"]}
467
+
```
468
+
469
+
This is where the `-` marker shines - it appends to the end:
470
+
471
+
```sh
472
+
$ jsonpp add '{"foo":["a","b"]}' '/foo/-' '"c"'
473
+
{"foo":["a","b","c"]}
474
+
```
475
+
476
+
### Remove
477
+
478
+
The `remove` operation deletes a value:
479
+
480
+
```sh
481
+
$ jsonpp remove '{"foo":"bar","baz":"qux"}' '/baz'
482
+
{"foo":"bar"}
483
+
```
484
+
485
+
For arrays, it removes and shifts:
486
+
487
+
```sh
488
+
$ jsonpp remove '{"foo":["a","b","c"]}' '/foo/1'
489
+
{"foo":["a","c"]}
490
+
```
491
+
492
+
### Replace
493
+
494
+
The `replace` operation updates an existing value:
495
+
496
+
```sh
497
+
$ jsonpp replace '{"foo":"bar"}' '/foo' '"baz"'
498
+
{"foo":"baz"}
499
+
```
500
+
501
+
Unlike `add`, `replace` requires the target to already exist:
502
+
503
+
```sh
504
+
$ jsonpp replace '{"foo":"bar"}' '/nonexistent' '"value"'
505
+
ERROR: JSON Pointer: member 'nonexistent' not found
506
+
File "-":
507
+
```
508
+
509
+
### Move
510
+
511
+
The `move` operation relocates a value:
512
+
513
+
```sh
514
+
$ jsonpp move '{"foo":{"bar":"baz"},"qux":{}}' '/foo/bar' '/qux/thud'
515
+
{"foo":{},"qux":{"thud":"baz"}}
516
+
```
517
+
518
+
### Copy
519
+
520
+
The `copy` operation duplicates a value:
521
+
522
+
```sh
523
+
$ jsonpp copy '{"foo":{"bar":"baz"}}' '/foo/bar' '/foo/qux'
524
+
{"foo":{"bar":"baz","qux":"baz"}}
525
+
```
526
+
527
+
### Test
528
+
529
+
The `test` operation verifies a value (useful in JSON Patch):
530
+
531
+
```sh
532
+
$ jsonpp test '{"foo":"bar"}' '/foo' '"bar"'
533
+
true
534
+
```
535
+
536
+
```sh
537
+
$ jsonpp test '{"foo":"bar"}' '/foo' '"baz"'
538
+
false
539
+
```
540
+
541
+
## Deeply Nested Structures
542
+
543
+
JSON Pointer handles arbitrarily deep nesting:
544
+
545
+
```sh
546
+
$ jsonpp eval rfc6901_example.json "/foo/0"
547
+
OK: "bar"
548
+
```
549
+
550
+
For deeper structures, just add more path segments. With nested objects:
551
+
552
+
```sh
553
+
$ jsonpp add '{"a":{"b":{"c":"d"}}}' '/a/b/x' '"y"'
554
+
{"a":{"b":{"c":"d","x":"y"}}}
555
+
```
556
+
557
+
With nested arrays:
558
+
559
+
```sh
560
+
$ jsonpp add '{"arr":[[1,2],[3,4]]}' '/arr/0/1' '99'
561
+
{"arr":[[1,99,2],[3,4]]}
562
+
```
563
+
564
+
## Summary
565
+
566
+
JSON Pointer (RFC 6901) provides a simple but powerful way to address
567
+
values within JSON documents:
568
+
569
+
1. **Syntax**: Pointers are strings of `/`-separated reference tokens
570
+
2. **Escaping**: Use `~0` for `~` and `~1` for `/` in tokens
571
+
3. **Evaluation**: Tokens navigate through objects (by key) and arrays (by index)
572
+
4. **URI Encoding**: Pointers can be percent-encoded for use in URIs
573
+
5. **Mutations**: Combined with JSON Patch (RFC 6902), pointers enable structured updates
574
+
575
+
The `jsont-pointer` library implements all of this with type-safe OCaml
576
+
interfaces, integration with the `jsont` codec system, and proper error
577
+
handling for malformed pointers and missing values.
+1
-1
test/dune
+1
-1
test/dune
+7
test/eval.t
+7
test/eval.t
···
48
48
Error: nonexistent member:
49
49
$ ./test_pointer.exe eval data/rfc6901_example.json "/nonexistent"
50
50
ERROR: JSON Pointer: member 'nonexistent' not found
51
+
File "-":
51
52
52
53
Error: index out of bounds:
53
54
$ ./test_pointer.exe eval data/rfc6901_example.json "/foo/2"
54
55
ERROR: JSON Pointer: index 2 out of bounds (array has 2 elements)
56
+
File "-":
55
57
$ ./test_pointer.exe eval data/rfc6901_example.json "/foo/99"
56
58
ERROR: JSON Pointer: index 99 out of bounds (array has 2 elements)
59
+
File "-":
57
60
58
61
Error: invalid array index (not a valid integer):
59
62
$ ./test_pointer.exe eval data/rfc6901_example.json "/foo/bar"
60
63
ERROR: JSON Pointer: invalid array index 'bar'
64
+
File "-":
61
65
62
66
Error: end marker not allowed in get:
63
67
$ ./test_pointer.exe eval data/rfc6901_example.json "/foo/-"
64
68
ERROR: JSON Pointer: '-' (end marker) refers to nonexistent array element
69
+
File "-":
65
70
66
71
Error: navigating through primitive (string):
67
72
$ ./test_pointer.exe eval data/rfc6901_example.json "/foo/0/0"
68
73
ERROR: JSON Pointer: cannot index into string with '0'
74
+
File "-":
69
75
$ ./test_pointer.exe eval data/rfc6901_example.json "/foo/0/bar"
70
76
ERROR: JSON Pointer: cannot index into string with 'bar'
77
+
File "-":
71
78
72
79
Nested evaluation with deep nesting:
73
80
$ cat data/nested.json
+5
test/mutations.t
+5
test/mutations.t
···
79
79
Error: remove nonexistent:
80
80
$ ./test_pointer.exe remove '{"foo":"bar"}' '/baz'
81
81
ERROR: JSON Pointer: member 'baz' not found for remove
82
+
File "-":
82
83
83
84
Error: replace nonexistent:
84
85
$ ./test_pointer.exe replace '{"foo":"bar"}' '/baz' '"qux"'
85
86
ERROR: JSON Pointer: member 'baz' not found
87
+
File "-":
86
88
87
89
Error: add to out of bounds index:
88
90
$ ./test_pointer.exe add '{"foo":["bar"]}' '/foo/5' '"qux"'
89
91
ERROR: JSON Pointer: index 5 out of bounds for add (array has 1 elements)
92
+
File "-":
90
93
91
94
Add nested path (parent must exist):
92
95
$ ./test_pointer.exe add '{"foo":{}}' '/foo/bar' '"baz"'
···
137
140
Error: remove from nonexistent array index:
138
141
$ ./test_pointer.exe remove '{"foo":["bar"]}' '/foo/5'
139
142
ERROR: JSON Pointer: index 5 out of bounds for remove
143
+
File "-":
140
144
141
145
Error: move to descendant of source (path doesn't exist after removal):
142
146
$ ./test_pointer.exe move '{"a":{"b":"c"}}' '/a' '/a/b'
143
147
ERROR: JSON Pointer: member 'a' not found
148
+
File "-":
144
149
145
150
Copy to same location (no-op essentially):
146
151
$ ./test_pointer.exe copy '{"foo":"bar"}' '/foo' '/foo'
+6
-35
test/test_pointer.ml
+6
-35
test/test_pointer.ml
···
7
7
close_in ic;
8
8
s
9
9
10
-
(* Convert Yojson.Safe.t to Jsont.json *)
11
-
let rec yojson_to_jsont (y : Yojson.Safe.t) : Jsont.json =
12
-
match y with
13
-
| `Null -> Jsont.Json.null ()
14
-
| `Bool b -> Jsont.Json.bool b
15
-
| `Int i -> Jsont.Json.number (float_of_int i)
16
-
| `Float f -> Jsont.Json.number f
17
-
| `String s -> Jsont.Json.string s
18
-
| `List l -> Jsont.Json.list (List.map yojson_to_jsont l)
19
-
| `Assoc pairs ->
20
-
let members = List.map (fun (k, v) ->
21
-
Jsont.Json.mem (Jsont.Json.name k) (yojson_to_jsont v)
22
-
) pairs in
23
-
Jsont.Json.object' members
24
-
| `Intlit s -> Jsont.Json.number (float_of_string s)
25
-
26
-
(* Convert Jsont.json to Yojson.Safe.t for output *)
27
-
let rec jsont_to_yojson (j : Jsont.json) : Yojson.Safe.t =
28
-
match j with
29
-
| Jsont.Null _ -> `Null
30
-
| Jsont.Bool (b, _) -> `Bool b
31
-
| Jsont.Number (f, _) ->
32
-
if Float.is_integer f && Float.abs f < 2e15 then
33
-
`Int (int_of_float f)
34
-
else
35
-
`Float f
36
-
| Jsont.String (s, _) -> `String s
37
-
| Jsont.Array (l, _) -> `List (List.map jsont_to_yojson l)
38
-
| Jsont.Object (members, _) ->
39
-
`Assoc (List.map (fun ((name, _), v) ->
40
-
(name, jsont_to_yojson v)
41
-
) members)
42
-
43
10
let parse_json s =
44
-
yojson_to_jsont (Yojson.Safe.from_string s)
11
+
match Jsont_bytesrw.decode_string Jsont.json s with
12
+
| Ok json -> json
13
+
| Error e -> failwith e
45
14
46
15
let json_to_string json =
47
-
Yojson.Safe.to_string (jsont_to_yojson json)
16
+
match Jsont_bytesrw.encode_string Jsont.json json with
17
+
| Ok s -> s
18
+
| Error e -> failwith e
48
19
49
20
(* Test: parse pointer and print indices *)
50
21
let test_parse pointer_str =