Sign in

    moonjson

    moonjson — the JSON family for MoonBit: RFC 8259 JSON, JSONC, JSON5 and JSON Lines over one data model, with JSON Pointer, and failures that name the line and column.

    json
    json5
    jsonc
    jsonlines
    ndjson
    json-pointer
    parser
    moonbit
    Download zip
    Version
    0.1.0
    License
    Apache-2.0
    Last updated
    last month
    Downloads
    1K

    No dependencies

    #moonjson

    The JSON family for MoonBit: one tree, several ways of writing it.

    let config = @json.parse("{\"host\":\"localhost\",\"port\":8080}")
    @json.write(config) // {"host":"localhost","port":8080}
    @json.write(config, indent=2) // over several lines

    // The same document, written the way a settings file is written.
    @jsonc.parse("{\"port\": 8080, // the one we bound\n}")

    // One place in a document, named (RFC 6901).
    @pointer.get(config, "/host") // "localhost"

    Run moon run examples/tour for the whole surface in one go.

    #Packages

    PackageWhatSpecification
    jsonJSON, exactlyRFC 8259, ECMA-404
    jsoncJSON with comments and trailing commaswhat VS Code accepts
    json5JSON5: unquoted names, single quotes, hexadecimal, Infinityspec.json5.org
    linesJSON Lines / NDJSON: one document to a linejsonlines.org
    pointerJSON PointerRFC 6901
    moonjsonThe scanner the dialects share, the writer, and Flavor—

    Each has the same four faces — parse, parse_bytes, write, write_bytes — so learning one is learning all of them. Writing is always strict JSON, whatever the document was written in.

    #The tree

    The data model is core's Json, not one of our own. Anything that already speaks @json.Json speaks this, with no conversion.

    A number keeps the text it was written as, so a document read and written again comes back unchanged — including a twenty-digit integer and 1e400, which a double cannot hold and which a parser that goes through one silently rewrites. A number JSON has no syntax for, which JSON5 can produce, is written as null, as every JavaScript implementation writes it.

    Members keep their order.

    #Failures

    Reading raises Malformed, which says what went wrong and where — offset, line and column:

    try @json.parse(source) catch {
    e => println("line \{e.at().line}, column \{e.at().column}")
    }

    Nesting is refused past 500 levels rather than taken to the stack: a document of a hundred thousand open brackets is a denial of service, not a document.

    A lone half of a surrogate pair is refused. It is not a character, it cannot be written back out, and any value put in its place would be a guess.

    #What is checked

    The strict parser is measured against cases drawn from JSONTestSuite, with every verdict taken from a third-party parser rather than asserted from memory — 95 documents that must be accepted or refused, and are. JSON5 is measured against the document json5.org puts on its front page, and JSON Pointer against every example RFC 6901 §5 prints.

    #What is not here yet

    Hjson and the other text dialects; CBOR, MessagePack, BSON and the other binary encodings; PostgreSQL's and SQLite's jsonb; JSON Patch, JSONPath, JSON Schema and canonical serialisation. They are planned in that order; the tracking list lives with the project.

    YAML and TOML are their own repositories. HOCON and HCL belong with configuration, because their substitutions and merges are evaluation, not syntax.

    #Install

    moon add moonbitstack/moonjson

    #Licence

    Apache-2.0.

    Malformed

    pub(all) suberror Malformed {
    Unexpected(At, Char)
    Truncated(At)
    BadEscape(At)
    BadNumber(At)
    Trailing(At)
    TooDeep(At)
    BadUtf8(At)
    } derive(Eq,
    Debug
    )

    Why a document could not be read, and where.

    Malformed::at

    fn Malformed::at(self : Malformed) -> At

    Where an error happened, as a person would say it.

    Malformed::equal

    fn Malformed::equal(Malformed, Malformed) -> Bool

    Malformed::not_equal

    fn Malformed::not_equal(x : Malformed, y : Malformed) -> Bool

    pub(all) struct At {
    at : Int
    line : Int
    column : Int
    } derive(Eq,
    Debug
    )

    Where something went wrong.

    The offset is in UTF-16 code units from the start, which is what a [StringView] indexes by; line and column are one-based, because that is how every editor counts and an error a person cannot find in their file is half an error.

    At::equal

    fn At::equal(At, At) -> Bool

    At::not_equal

    fn At::not_equal(x : At, y : At) -> Bool

    At::to_repr

    Flavor

    pub(all) struct Flavor {
    comments : Bool
    trailing_commas : Bool
    unquoted_keys : Bool
    single_quotes : Bool
    extra_numbers : Bool
    extra_escapes : Bool
    extra_space : Bool
    } derive(Eq,
    Debug
    )

    What a dialect permits beyond RFC 8259.

    The dialects in this family differ only in their syntax — the tree they produce is the same — so the scanner is written once and each dialect is a set of switches. A dialect outside this repository can be described the same way without forking anything.

    Flavor::equal

    fn Flavor::equal(Flavor, Flavor) -> Bool

    Flavor::not_equal

    fn Flavor::not_equal(x : Flavor, y : Flavor) -> Bool

    Flavor::to_repr

    commented

    let commented : Flavor

    JSON with comments and trailing commas — what editors and their configuration files have settled on, tsconfig.json being the best-known.

    depth_limit

    let depth_limit : Int

    How deep the nesting may go.

    Five hundred is past anything a document written for a reader contains and far short of what exhausts a stack on any backend. JSONTestSuite's hundred-thousand-bracket cases are meant to be refused, not survived.

    read

    fn read(input : StringView, flavor : Flavor) -> Json raise Malformed

    Read one document in the given dialect.

    The whole input must be one value: anything after it, other than whitespace and — where the dialect allows them — comments, is [Trailing], not silently ignored. A parser that stops at the first value turns a truncated file into a plausible one.

    read_bytes

    fn read_bytes(input : BytesView, flavor : Flavor) -> Json raise Malformed

    Read one document from bytes, which is how one arrives over a wire.

    relaxed

    let relaxed : Flavor

    JSON5: everything ECMAScript 5 would have accepted.

    strict

    let strict : Flavor

    RFC 8259 and nothing more.

    write

    fn write(value : Json, indent? : Int) -> String

    Write a tree as strict JSON, whatever dialect it was read from.

    indent of zero, the default, writes the compact form with no space anywhere; anything larger writes one line per member with that many spaces a level, and a space after each colon.

    A number that came in as text goes back out as that text, so a document read and written again is unchanged. Only a number with no such text is formatted, and a number that is not finite — which JSON5 can produce and JSON cannot write — becomes null, as every JavaScript implementation does.

    write_bytes

    fn write_bytes(value : Json, indent? : Int) -> Bytes

    The same, as UTF-8 bytes — the form a body goes onto a wire in.