Every output below is the real one
Each snippet is the one in src/examples/tour or a variant of
it, and each block under output is what it prints. Run the
whole tour with moon run src/examples/tour.
One document, three ways of writing it
The dialects differ only in their syntax — the tree they produce is the same, so the three documents compare equal.
let plain = @json.loads("{\"host\":\"localhost\",\"port\":8080,\"tls\":false}")
let commented = @jsonc.loads(
#|{
#| "host": "localhost", // where to bind
#| "port": 8080,
#| "tls": false, // a certificate is not set up yet
#|}
)
let loose = @json5.loads("{host:'localhost',port:8080,tls:false,}")
println("jsonc == json -> \{commented == plain}")
println("json5 == json -> \{loose == plain}")
jsonc == json -> true json5 == json -> true
Writing it back, whatever it was written in
Writing is always strict JSON. indent puts one member
to a line, sort writes members in order of name, and
ascii escapes everything above U+007E.
let cfg = @json.loads("{\"host\":\"localhost\",\"port\":8080,\"tags\":[\"a\",\"b\"]}")
println(@json.dumps(cfg))
println(@json.dumps(cfg, indent=2))
println(@json.dumps(cfg, sort=true))
{"host":"localhost","port":8080,"tags":["a","b"]}
{
"host": "localhost",
"port": 8080,
"tags": [
"a",
"b"
]
}
{"host":"localhost","port":8080,"tags":["a","b"]}
A number keeps the digits it came with
1e400 is past what a double can hold, and a
twenty-digit integer is past what it can hold exactly. Both come
back as they were written, because the tree keeps the text.
let wide = "[1e400,12345678901234567890,0.1]" println(@json.dumps(@json.loads(wide)) == wide)
true
A stream of records, one to a line
JSON Lines is what a log file is written as: a reader can start anywhere and a writer can append without rewriting. Blank lines are skipped rather than refused.
let records = @lines.loads_all("{\"n\":1}\n{\"n\":2}\n{\"n\":3}\n")
println("records -> \{records.length()}")
records -> 3
One place in a document, named
A JSON Pointer (RFC 6901) names one place. set answers
a new document — a member or an element that is already there is
replaced, one that is not is created, and - appends —
and remove takes one away.
let tree = @json.loads("{\"a\":{\"b\":[10,20,30]}}")
println(@json.dumps(@pointer.get(tree, "/a/b/1")))
println(@pointer.has(tree, "/a/b/9"))
let grown = @pointer.set(tree, "/a/b/-", Json::number(40))
println(@json.dumps(grown))
println(@json.dumps(@pointer.remove(tree, "/a/b/0")))
20
false
{"a":{"b":[10,20,30,40]}}
{"a":{"b":[20,30]}}
A patch, applied in order — or not at all
A JSON Patch (RFC 6902) is an array of six operations. Here a
test guards a replace: when the test
holds the patch applies, and when it does not, nothing changes —
the document it was given is untouched either way.
let spec = @json.loads("{\"spec\":{\"replicas\":1,\"image\":\"greeter:1\"}}")
let ops = @patch.loads(
"[{\"op\":\"test\",\"path\":\"/spec/replicas\",\"value\":1},{\"op\":\"replace\",\"path\":\"/spec/replicas\",\"value\":5}]"
)
println(@json.dumps(@patch.apply(spec, ops)))
let wrong = @patch.loads(
"[{\"op\":\"test\",\"path\":\"/spec/replicas\",\"value\":9},{\"op\":\"replace\",\"path\":\"/spec/replicas\",\"value\":5}]"
)
try @patch.apply(spec, wrong) |> ignore catch {
e => println("refused: \{e.to_repr()}")
} noraise {
_ => ()
}
{"spec":{"replicas":5,"image":"greeter:1"}}
refused: TestFailed(path="/spec/replicas")
A merge patch that looks like the document
A JSON Merge Patch (RFC 7396) needs no operations: a member that is
not there is added, one that is there is replaced, and a member
whose value is null is taken away. Here the replica
count is replaced and the image is removed.
let spec = @json.loads("{\"spec\":{\"replicas\":1,\"image\":\"greeter:1\"}}")
let patch_text = "{\"spec\":{\"replicas\":5,\"image\":null}}"
println(@json.dumps(@merge.merge(spec, @json.loads(patch_text))))
{"spec":{"replicas":5}}
A failure names the line and the column
Reading raises Refused with the offset, the line and
the column — an error a person cannot find in their file is half an
error.
try @json.loads("{\n \"a\": 1,\n \"b\": ?\n}") |> ignore catch {
e => println("line \{e.at().line}, column \{e.at().column}")
} noraise {
_ => ()
}
line 3, column 8
Hjson and CSON: syntax a person types
Hjson drops the braces, the quotes around names and the commas; CSON drops the braces and uses indentation. Both read to the same tree as JSON, so what a person typed and what a machine wrote compare equal.
let from_hjson = @hjson.loads("{ a: 1, b: [2, 3] }")
let from_cson = @cson.loads("a: 1\nb:\n - 2\n - 3\n")
println(@json.dumps(from_hjson, sort=true))
println(@json.dumps(from_cson, sort=true))
{"a":1,"b":[2,3]}
{"a":1,"b":[2,3]}
A formatter, not a dialect
pretty lays a tree out with the knobs a formatter wants —
spaces or tabs a level, LF or CRLF, names sorted — and
reformat reads a document and writes it back, the way
python -m json.tool does.
let cfg = @json.loads("{\"tls\":false,\"host\":\"localhost\",\"port\":8080}")
println(@pretty.format(cfg, style=@pretty.Format::new(indent=4, sort=true)))
{
"host": "localhost",
"port": 8080,
"tls": false
}
A compact binary: CBD
CBD packs a tree into a five-byte header, a dictionary of its keys and a type byte per value. The dictionary is what makes a document of repeated names small, and the encoder reproduces the specification's own example byte for byte.
let body = @cbd.dump(@json.loads("{\"name\":\"John\",\"age\":30}"))
println(@hex.encode(body[:]))
println(@cbd.load(body[:]) == @json.loads("{\"name\":\"John\",\"age\":30}"))
cbd1010002046e616d6503616765a1020160044a6f686e02401e true
A stream, written and read one at a time
A sink writes a sequence of documents one to a line; a
source reads them back one at a time, without parsing the
whole stream first.
let sink = @sink.Sink::new()
sink.write(@json.loads("{\"n\":1}"))
sink.write(@json.loads("{\"n\":2}"))
println(sink.length())
let src = @source.Source::new(sink.to_bytes()[:])
println(@json.dumps(src.read().unwrap()))
2
{"n":1}
One tree, any two notations
The promise the family makes: a value read from one notation and written to another is unchanged. JSON in, CBOR out, SQLite jsonb on, PostgreSQL jsonb back — the tree that comes out is the tree that went in.
let t = @json.loads("{\"a\":1,\"b\":[true,null]}")
let cbor = @cbor.dump(t)
let sqlite = @sqlite.dump(@cbor.load(cbor[:]))
let pg = @pg.dump(@sqlite.load(sqlite[:]))
println(@pg.load(pg[:]) == t)
true