A JSON parser and serializer for Carp.
(load "git@github.com:carpentry-org/json@0.6.0")Parse a JSON string into a JSON value using JSON.parse:
(match (JSON.parse "{\"name\": \"carp\", \"version\": 1}")
(Result.Success j) (println* &j)
(Result.Error e) (IO.errorln &(JSON.parse-error-str &e)))JSON.parse returns a (Result JSON ParseError). The error carries both
a ParseErrorKind (one of 16 variants describing what went wrong) and a
byte position. Use JSON.parse-error-str to format it for display, or
match-ref on the kind to react programmatically.
You can construct JSON values directly:
(def j (JSON.obj [(JSON.entry @"name" (JSON.Str @"carp"))
(JSON.entry @"version" (JSON.Num 1.0))
(JSON.entry @"active" (JSON.Bool true))]))Or convert native Carp values via the to-json interface, which is
implemented for Bool, Int, Long, Float, Double, String, and
Array:
(to-json @"hello") ; => (JSON.Str "hello")
(to-json [1 2 3]) ; => (JSON.Arr [...])
(to-json [@"a" @"b"]) ; => (JSON.Arr [(JSON.Str "a") (JSON.Str "b")])Convert any JSON value back to a string with str:
(match (JSON.str &j)
(Result.Success s) (println &s)
(Result.Error e) (IO.errorln &(JSON.serialize-error-str &e)))JSON.str returns a (Result String SerializeError). It only fails when
a JSON.Num contains NaN or infinity, neither of which is representable
in JSON.
; Look up a key in an object
(JSON.get &j "name") ; => (Maybe.Just (JSON.Str "carp"))
; Index into an array
(JSON.nth &arr 0) ; => (Maybe.Just ...)
; Nested key lookup
(JSON.get-in &j &[@"data" @"users"])
; Extract typed values
(JSON.as-str &(JSON.Str @"hi")) ; => (Maybe.Just "hi")
(JSON.as-num &(JSON.Num 3.14)) ; => (Maybe.Just 3.14)
(JSON.as-bool &(JSON.Bool true)) ; => (Maybe.Just true)
; Compare structurally: arrays by order, objects independent of member order
(= &(JSON.Num 1.0) &(JSON.Num 1.0)) ; => trueJSON.Pointer implements RFC 6901,
which addresses a value inside a document with a string like /foo/0/bar:
(JSON.Pointer.get &doc "/foo/0") ; => (Maybe.Just ...)
(JSON.Pointer.get &doc "") ; => the whole document
(JSON.Pointer.get &doc "/a~1b") ; => the value under the key "a/b"Array tokens must be 0 or a positive integer without a leading zero; the
- end-of-array token, out-of-range indices, and invalid pointers all return
Nothing. Use escape/unescape to build or decode a single reference token
(~ becomes ~0, / becomes ~1):
(JSON.Pointer.escape "a/b") ; => "a~1b"
(JSON.Pointer.unescape "m~0n") ; => "m~n"JSON.Patch implements RFC 6902,
which expresses a change to a document as a JSON array of operation objects
addressed by JSON Pointer:
(def patch
(Result.unsafe-from-success
(JSON.parse "[{\"op\": \"add\", \"path\": \"/tags/-\", \"value\": \"lisp\"},
{\"op\": \"replace\", \"path\": \"/version\", \"value\": 2},
{\"op\": \"move\", \"from\": \"/old\", \"path\": \"/new\"}]")))
(match (JSON.Patch.apply &doc &patch)
(Result.Success patched) (println* &patched)
(Result.Error e) (IO.errorln &(JSON.patch-error-str &e)))All six operations are supported. add inserts into an array (shifting the
rest right, with - appending) and inserts or replaces an object member;
remove and replace require the location to exist; move may not move a
location into one of its own children; test compares the addressed value
with JSON.=. The empty pointer "" addresses the whole document.
Patches are atomic: if any operation fails, the returned PatchError names
the index of the failing operation and the input document is left untouched.
Two limits bound the work a patch can ask for, since copy duplicates a
subtree and a short patch could otherwise build a very large document. An
operation that would nest the document deeper than JSON.parse accepts is
rejected with DepthLimitExceeded, and a patch may insert at most
json-max-patch-nodes nodes beyond the combined size of the document and the
patch before SizeLimitExceeded. Neither binds on a hand-written patch.
JSON.Patch.diff goes the other way, building a patch that takes one document
to another:
(def a (Result.unsafe-from-success (JSON.parse "{\"a\":1,\"b\":{\"c\":2}}")))
(def b (Result.unsafe-from-success (JSON.parse "{\"a\":1,\"b\":{\"c\":3}}")))
(JSON.Patch.diff &a &b) ; => [{"op":"replace","path":"/b/c","value":3}]
(JSON.Patch.apply &a &(JSON.Patch.diff &a &b)) ; => (Success b)Objects are compared member by member and arrays element by element, so a
change deep inside a document costs one operation addressed by pointer rather
than a copy of the value enclosing it. A key only in the target becomes an
add, a key only in the source a remove, and anything else that differs a
replace, including two documents of different kinds, which replace the whole
document at the empty pointer. Keys are escaped, and a shrinking array is
trimmed in descending index order so that every pointer still names the
element it did before the trim.
Every operation diff emits is one apply accepts, so the round trip holds
for any pair of documents, null members and array edits included.
JSON.merge-patch implements RFC 7386,
which is what an HTTP API almost always means by application/merge-patch+json.
The patch is a document shaped like the target, and merging it replaces the
members it mentions:
(def doc (Result.unsafe-from-success (JSON.parse "{\"a\":1,\"b\":{\"c\":2}}")))
(def patch (Result.unsafe-from-success (JSON.parse "{\"b\":{\"c\":3},\"d\":4}")))
(JSON.merge-patch @&doc &patch) ; => {"a":1,"b":{"c":3},"d":4}A patch that is not an object replaces the target outright, a null member
deletes that key, and a target that is not an object is treated as an empty
one. Arrays are replaced whole, never merged element by element.
JSON.merge-diff builds the smallest patch taking one document to another,
emitting null for dropped members and omitting unchanged ones:
(JSON.merge-diff &doc &other) ; => {"b":{"c":3},"d":4}
(JSON.merge-patch @&doc &(JSON.merge-diff &doc &other)) ; => otherTwo things the format cannot express: setting a member to null, since that
spelling already means delete, and reaching inside an array. So a merge-diff
whose target introduces a null member does not survive the round trip, and
an array edit costs the whole array. Reach for JSON.Patch when you need
either.
(JSON.null? &(JSON.Null)) ; => true
(JSON.bool? &(JSON.Null)) ; => false
(JSON.num? &(JSON.Num 1.0)) ; => true
(JSON.str? &(JSON.Str @"")) ; => true
(JSON.arr? &(JSON.Arr [])) ; => true
(JSON.obj? &(JSON.Obj {})) ; => trueJSON is a sum type with six variants:
(deftype JSON
(Null [])
(Bool [Bool])
(Num [Double])
(Str [String])
(Arr [(Array (Box JSON))])
(Obj [(Map String (Box JSON))]))Arrays and objects contain Boxed values because the type is recursive.
carp -x test/json.carp
carp -x bench/json_bench.carp
Have fun!