JSON, its dialects and its binary encodings for MoonBit.
let config = @json.loads("{\"host\":\"localhost\",\"port\":8080}")
assert_eq(@json.dumps(config), "{\"host\":\"localhost\",\"port\":8080}")
assert_eq(@json.dumps(config, indent=2), "{\n \"host\": \"localhost\",\n \"port\": 8080\n}")
// The same document, written the way a settings file is written.
let source =
#|{
#| "port": 8080, // the one we bound
#|}
assert_eq(@jsonc.loads(source[:]), @json.loads("{\"port\":8080}"))
// One place in a document, named (RFC 6901).
assert_eq(@json.dumps(@pointer.get(config, "/port")), "8080")| text → tree | bytes → tree | tree → text | tree → bytes | |
|---|---|---|---|---|
| one document | loads | load | dumps | dump |
| a stream of them | loads_all | load_all | dumps_all | dump_all |
// Read a document, change one field, write it back.
let text =
#|{
#| "spec": { "replicas": 1, "image": "app:1" }
#|}
let cfg = @jsonc.loads(text[:])
let wanted = @pointer.set(cfg, "/spec/replicas", Json::number(5))
assert_eq(@json.dumps(wanted), "{\"spec\":{\"replicas\":5,\"image\":\"app:1\"}}")| Package | What | Specification |
|---|---|---|
| json | JSON, exactly | RFC 8259, ECMA-404 |
| jsonc | JSON with comments and trailing commas | what VS Code accepts |
| json5 | JSON5: unquoted names, single quotes, hexadecimal, Infinity | spec.json5.org |
| lines | JSON Lines / NDJSON: one document to a line | jsonlines.org |
| pointer | JSON Pointer | RFC 6901 |
| patch | JSON Patch: add, remove, replace, move, copy, test | RFC 6902 |
| merge | JSON Merge Patch | RFC 7396 |
| path | JSONPath: a query language over the tree, filters and all | RFC 9535 |
| jcs | Canonical JSON: one document, one sequence of bytes | RFC 8785 |
| cbor | CBOR, the binary encoding JSON's own working group wrote | RFC 8949 |
| msgpack | MessagePack | msgpack.org |
| bson | BSON, what MongoDB stores and sends | bsonspec.org |
| ubjson | Universal Binary JSON | ubjson.org |
| smile | Smile, Jackson's binary JSON | github.com/FasterXML/smile-format-specification |
| bjson | Binary JSON, the bjson.org draft | bjson.org |
| krson | KRSON, a schemaless binary JSON | crates.io/crates/krson-core |
| cbd | CompactBinaryData: a 5-byte header, a key dictionary, 3-bit type descriptors | github.com/makalin/CBD |
| jsonb/sqlite | SQLite's jsonb | sqlite.org/jsonb.html |
| jsonb/pg | PostgreSQL's on-disk jsonb | postgresql.org/docs/current/datatype-json |
| hjson | Hjson: comments, quoteless keys, commas optional | hjson.github.io |
| cson | CSON, CoffeeScript-style indentation | github.com/bevry/cson |
| superjson | SuperJSON | github.com/blitz-js/superjson |
| ntv | Named and typed values | draft-thomy-json-ntv-03 |
| rjson | Ruby's JSON | ruby-doc.org |
| json11 | dropbox/json11 | github.com/dropbox/json11 |
| jsonz | json-z | npmjs.com/package/json-z |
| zson | ZSON | github.com/postgrespro/zson |
| zjson | Zed's ZJSON | github.com/brimdata/zed |
| sink | Streaming NDJSON writer | ndjson-spec |
| source | Streaming NDJSON reader | ndjson-spec |
| pretty | Formatter: spaces or tabs, CRLF, sorted members; text-to-text reformat | — |
| moonjson | The scanner the dialects share, the writer, and Refused | — |
| Value | Marker |
|---|---|
| a byte string | {"$bytes": "0102"} — lowercase hex, no separators |
| a tag or an extension type | {"$tag": 1, "value": …}, {"$ext": 1, "data": "0102"} |
| undefined | {"$undefined": true} |
| a map whose keys are not strings | {"$map": [[1, 2]]} |
let doc = @json5.loads("{host:'localhost',port:8080,tls:false}")
let body = @cbor.dump(doc) // the same tree, as bytes
assert_eq(@cbor.load(body[:]), doc) // and back, the same treelet same = @jsonc.loads(text[:]) // strict JSON, plus comments
assert_eq(same, @json.loads(text[:])) // and the same tree comes out
let shallow = @json.loads(text[:], flavor=@moonjson.Flavor::new(depth=100))
assert_eq(shallow, same)
let first = @json.loads(
text[:],
flavor={ ..@moonjson.strict, duplicates: @moonjson.Duplicates::First },
)
assert_eq(first, same)let city = @json.loads("{\"city\":\"\u{5965}\u{514b}\u{5170}\"}")
assert_eq(@json.dumps(city, ascii=true), "{\"city\":\"\\u5965\\u514b\\u5170\"}")
let pair = @json.loads("{\"port\":8080,\"host\":\"localhost\"}")
assert_eq(@json.dumps(pair, sort=true), "{\"host\":\"localhost\",\"port\":8080}")| Setting | Default | Why that one |
|---|---|---|
| depth | 500 | A hundred thousand open brackets is a denial of service, not a document |
| surrogates | refuse | The case is implementation-defined, so the safer side is the default: a lone half cannot be written back out |
| duplicates | Last | The behaviour most readers already have |
| bom | skipped | RFC 8259 §8.1 forbids writing one and says nothing about reading one; refusing would reject half of what Windows tooling produces |
| ascii | off | A UTF-8 body is the normal case now |
| sort | off | The order a document was written in is information; a diff is the reason to discard it, and a diff can ask |
| CBOR deterministic | on | RFC 8949 §4.2 recommends it, and a hash or a signature over an encoding needs it. It sorts the members of a map; off still writes valid CBOR, only longer, and in the order the document had |
| CBOR depth | 500 | The same as the text reader, for the same reason |
| MessagePack floats | Double | No precision is lost unless asked |
| BSON depth | 500 | Documents nest inside documents exactly as brackets nest inside brackets |
let wide = "[1e400,12345678901234567890,0.1]"
assert_eq(@json.dumps(@json.loads(wide)), wide)// One field, and nothing else changes.
let patched = @pointer.set(cfg, "/spec/replicas", Json::number(5))
// Two operations, applied in order — or, if one fails, not at all.
let ops = @patch.loads(
"[{\"op\":\"test\",\"path\":\"/spec/replicas\",\"value\":5},{\"op\":\"replace\",\"path\":\"/spec/image\",\"value\":\"app:2\"}]",
)
assert_eq(@json.dumps(@patch.apply(patched, ops)), "{\"spec\":{\"replicas\":5,\"image\":\"app:2\"}}")
// A patch that looks like the document: a member that is not there is added,
// one that is there is replaced, and a null takes one away.
assert_eq(@json.dumps(@merge.merge(patched, @json.loads("{\"spec\":{\"image\":null}}"))), "{\"spec\":{\"replicas\":5}}")let shelf = @json.loads("{\"store\":{\"book\":[{\"title\":\"A\",\"price\":8.95},{\"title\":\"B\",\"price\":12.99},{\"title\":\"C\",\"price\":8.99}]}}")
let cheap = @path.select(shelf, "$..book[?@.price < 10].title")
assert_eq(@json.dumps(Json::array(cheap)), "[\"A\",\"C\"]")
let query = @path.compile("$.store.book[*].title") // once
assert_eq(@json.dumps(Json::array(query.select(shelf))), "[\"A\",\"B\",\"C\"]")let broken =
#|{
#| "a": 1,
#| "b": ?
#|}
try @json.loads(broken[:]) |> ignore catch {
e => match e.at() {
Some(spot) => assert_eq("line \{spot.line}, column \{spot.column}", "line 3, column 8")
None => abort("a scanner refusal carries a place")
}
}try @json.loads(broken[:]) |> ignore catch {
e => assert_eq(e.message(), "line 3, column 8: unexpected '?'")
}moon add moonbitstack/moonjsonpub(all) suberror Refused {
Unexpected(At, Char)
Truncated(At)
BadEscape(At)
BadNumber(At)
Trailing(At)
TooDeep(At)
BadUtf8(At)
Repeated(At, String)
Short(at~ : Int)
Extra(at~ : Int)
Bad(at~ : Int, why~ : String)
Unwritable(why~ : String)
Syntax(path~ : String, at~ : Int)
Missing(path~ : String, token~ : String)
NotContainer(path~ : String, token~ : String)
BadIndex(path~ : String, token~ : String)
Document(why~ : String)
Operation(at~ : Int, why~ : String)
TestFailed(path~ : String)
IntoChild(from~ : String, path~ : String)
Root(path~ : String)
} derive(Eq, Debug)pub(all) struct Flavor {
comments : Bool
trailing_commas : Bool
unquoted_keys : Bool
single_quotes : Bool
extra_numbers : Bool
extra_escapes : Bool
extra_space : Bool
depth : Int
surrogates : Bool
duplicates : Duplicates
} derive(Eq, Debug)fn Flavor::new(comments? : Bool, trailing_commas? : Bool, unquoted_keys? : Bool, single_quotes? : Bool, extra_numbers? : Bool, extra_escapes? : Bool, extra_space? : Bool, depth? : Int, surrogates? : Bool, duplicates? : Duplicates) -> FlavorInstall
Download zipJSON, its dialects and its binary encodings for MoonBit.