examples

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.

01

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.

moonbit
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}")
output
jsonc == json -> true
json5 == json -> true
02

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.

moonbit
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))
output
{"host":"localhost","port":8080,"tags":["a","b"]}
{
  "host": "localhost",
  "port": 8080,
  "tags": [
    "a",
    "b"
  ]
}
{"host":"localhost","port":8080,"tags":["a","b"]}
03

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.

moonbit
let wide = "[1e400,12345678901234567890,0.1]"
println(@json.dumps(@json.loads(wide)) == wide)
output
true
04

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.

moonbit
let records = @lines.loads_all("{\"n\":1}\n{\"n\":2}\n{\"n\":3}\n")
println("records       -> \{records.length()}")
output
records       -> 3
05

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.

moonbit
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")))
output
20
false
{"a":{"b":[10,20,30,40]}}
{"a":{"b":[20,30]}}
06

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.

moonbit
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 {
  _ => ()
}
output
{"spec":{"replicas":5,"image":"greeter:1"}}
refused: TestFailed(path="/spec/replicas")
07

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.

moonbit
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))))
output
{"spec":{"replicas":5}}
08

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.

moonbit
try @json.loads("{\n  \"a\": 1,\n  \"b\": ?\n}") |> ignore catch {
  e => println("line \{e.at().line}, column \{e.at().column}")
} noraise {
  _ => ()
}
output
line 3, column 8
09

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.

moonbit
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))
output
{"a":1,"b":[2,3]}
{"a":1,"b":[2,3]}
10

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.

moonbit
let cfg = @json.loads("{\"tls\":false,\"host\":\"localhost\",\"port\":8080}")
println(@pretty.format(cfg, style=@pretty.Format::new(indent=4, sort=true)))
output
{
    "host": "localhost",
    "port": 8080,
    "tls": false
}
11

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.

moonbit
let body = @cbd.dump(@json.loads("{\"name\":\"John\",\"age\":30}"))
println(@hex.encode(body[:]))
println(@cbd.load(body[:]) == @json.loads("{\"name\":\"John\",\"age\":30}"))
output
cbd1010002046e616d6503616765a1020160044a6f686e02401e
true
12

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.

moonbit
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()))
output
2
{"n":1}
13

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.

moonbit
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)
output
true