JsonValue

:= [
    Null,
    Bool(Bool),
    Number(Str),
    String(Str),
    Array(List(JsonValue)),
    Object(List({ key : Str, value : JsonValue })),
]

A dynamic JSON value and parser based on RFC 8259.

Use this module when a record type cannot describe a document: keys that are not Roc identifiers (such as JSON-LD's @context), arrays whose objects differ in shape, JSON Schema files, or data that must pass through untouched. To decode JSON straight into your own record type, use the builtin Json instead.

A JsonValue is one of:

  • Null, Bool and String;
  • Number, holding the exact number text from the input, so integers of any size and decimals of any precision survive unchanged. Convert it with JsonValue.as_i64, JsonValue.as_u64 or JsonValue.as_dec;
  • Array, and Object, whose members keep their source order.

JsonValue.parse_str is strict: it accepts exactly the JSON of RFC 8259 and rejects duplicate keys. JsonValue.parse_with takes JsonValue.Options to accept common relaxations (comments, trailing commas, a byte order mark, NaN and Infinity, lenient strings), to add checks (I-JSON numbers, the top-level shape, size guards), and to choose what a repeated key means. Nesting is limited to 128 levels by default.

JsonValue.to_str and JsonValue.to_str_with write a value back out as compact or indented JSON, refusing values that would not read back the same.

Strings and numbers read from the input share its bytes instead of being copied, so a value keeps the whole input alive while any of them is.

expect {
    doc = JsonValue.parse_str("{\"@context\": \"https://spdx.org/rdf/3.0.1/spdx-context.jsonld\", \"@graph\": []}")?
    doc.keys() == ["@context", "@graph"]
}
is_eq : _

Compare two JSON values structurally. Numbers compare by their text, so Number("1.0") and Number("1") differ.

to_hash : _

Hash a JSON value, so values can be Dict keys and Set members.

validate : ValidationProps -> Try(JsonValue, [BadQuotedBytes(Str)])

Parse a complete document during compilation, reporting its source name and position. It accepts exactly what JsonValue.parse_with accepts with the same options.

expect JsonValue.validate({ source: "{\"a\": [1,]}", name: "list.json" }) == Err(BadQuotedBytes("list.json:1:9: JSON: trailing comma is not allowed"))

expect JsonValue.validate({ source: "{\"a\": [1,]}", options: { trailing_commas: True } }).map_ok(|doc| doc.get_path(["a", "0"])) == Ok(Ok(Number("1")))
from_quote : Str -> Try(JsonValue, [BadQuotedBytes(Str)])

Use a strict JSON document as a quoted value.

expect {
    settings : JsonValue
    settings = "{\"port\": 8080}"
    settings.get("port") == Ok(Number("8080"))
}

expect JsonValue.from_quote("{\"a\": 1, \"a\": 1}") == Err(BadQuotedBytes("<literal>:1:10: JSON: duplicate key \"a\""))
parse_str : Str -> Try(JsonValue, [InvalidJsonValue(Error)])

Parse one complete, strict RFC 8259 document. Whitespace may surround the value; anything else after it is an error.

expect JsonValue.parse_str("{\"a\": [true, null]}") == Ok(Object([{ key: "a", value: Array([Bool(True), Null]) }]))

expect JsonValue.parse_str("[1,]") == Err(InvalidJsonValue({ line: 1, column: 3, message: "trailing comma is not allowed" }))
parse_with : Str, Options -> Try(JsonValue, [InvalidJsonValue(Error)])

Parse one complete document with the given JsonValue.Options.

expect JsonValue.parse_with("{\"a\": 1, \"a\": 2}", { duplicates: KeepLast }) == Ok(Object([{ key: "a", value: Number("2") }]))
parser : Parser(Bytes, JsonValue)

A Parser that reads one strict JSON value and the whitespace around it, and leaves the rest of the input for the next parser. A failure is ParseError({ message, offset }) with the byte offset of the problem.

expect Utf8.parse_str_partial(JsonValue.parser, "[1] rest").map_ok(|{ rest, .. }| rest) == Ok("rest")

expect Utf8.parse_str(JsonValue.parser, "[1 2]") == Err(ParseError({ message: "expected ',' or ']' after an array item", offset: 3 }))
parser_with : Options -> Parser(Bytes, JsonValue)

Like JsonValue.parser, with the given JsonValue.Options. max_bytes bounds the supplied buffer, including any unconsumed suffix.

expect Utf8.parse_str(JsonValue.parser_with({ comments: True }), "1 // one") == Ok(Number("1"))
get : JsonValue, Str -> Try(JsonValue, [Missing])

The value of the first member named key in an object. Anything else, or an object without that key, is Err(Missing).

expect JsonValue.parse_str("{\"a\": 1}").map_ok(|doc| doc.get("a")) == Ok(Ok(Number("1")))
at : JsonValue, U64 -> Try(JsonValue, [Missing])

The item at a zero-based index of an array. Anything else, or an index past the end, is Err(Missing).

expect JsonValue.parse_str("[10, 20]").map_ok(|doc| doc.at(1)) == Ok(Ok(Number("20")))
get_path : JsonValue, List(Str) -> Try(JsonValue, [Missing])

Follow a path of object keys and array indexes. A segment applied to an array must be a decimal index such as "0", without a sign or leading zeros, as in a JSON Pointer (RFC 6901).

expect JsonValue.parse_str("{\"a\": [{\"b\": true}]}").map_ok(|doc| doc.get_path(["a", "0", "b"])) == Ok(Ok(Bool(True)))
keys : JsonValue -> List(Str)

The keys of an object, in order; empty for anything else.

expect JsonValue.parse_str("{\"b\": 1, \"a\": 2}").map_ok(|doc| doc.keys()) == Ok(["b", "a"])
as_str : JsonValue -> Try(Str, [WrongType])

The text of a String value, or Err(WrongType).

expect JsonValue.parse_str("\"caf\\u00e9\"").map_ok(|doc| doc.as_str()) == Ok(Ok("café"))
as_bool : JsonValue -> Try(Bool, [WrongType])

The boolean of a Bool value, or Err(WrongType).

expect JsonValue.parse_str("false").map_ok(|doc| doc.as_bool()) == Ok(Ok(False))
as_list : JsonValue -> Try(List(JsonValue), [WrongType])

The items of an Array value, or Err(WrongType).

expect JsonValue.parse_str("[null]").map_ok(|doc| doc.as_list()) == Ok(Ok([Null]))
as_object : JsonValue -> Try(List({ key : Str, value : JsonValue }), [WrongType])

The members of an Object value in order, or Err(WrongType).

expect JsonValue.parse_str("{\"a\": null}").map_ok(|doc| doc.as_object()) == Ok(Ok([{ key: "a", value: Null }]))
as_i64 : JsonValue -> Try(I64, [WrongType, OutOfRange])

The integer of a Number written as a JSON integer, with no fraction or exponent. Other values, including 1.0 and 1e3, are Err(WrongType); an integer outside the I64 range is Err(OutOfRange).

expect JsonValue.parse_str("-42").map_ok(|doc| doc.as_i64()) == Ok(Ok(-42))

expect JsonValue.parse_str("1e3").map_ok(|doc| doc.as_i64()) == Ok(Err(WrongType))
as_u64 : JsonValue -> Try(U64, [WrongType, OutOfRange])

Like JsonValue.as_i64, for U64. A negative integer other than -0 is Err(OutOfRange).

expect JsonValue.parse_str("18446744073709551615").map_ok(|doc| doc.as_u64()) == Ok(Ok(18446744073709551615))

expect JsonValue.parse_str("-1").map_ok(|doc| doc.as_u64()) == Ok(Err(OutOfRange))
as_dec : JsonValue -> Try(Dec, [WrongType, OutOfRange])

The value of a Number as a Dec. A number outside the Dec range, one that needs more than 18 decimal places (trailing zeros don't count, so 1.50000000000000000000 is 1.5), or NaN or Infinity, is Err(OutOfRange); anything else is Err(WrongType).

expect JsonValue.parse_str("-1.25e+2").map_ok(|doc| doc.as_dec()) == Ok(Ok(-125.0))
to_str : JsonValue -> Try(Str, [InvalidNumber(Str), DuplicateKey(Str), TooDeep, TooLarge(U64)])

Write a value as compact JSON, keeping member order.

A value built by hand can be one that JSON cannot express: a Number whose text is not a JSON number (including NaN and Infinity) is InvalidNumber, a key repeated in one object is DuplicateKey, and more than 128 levels of nesting is TooDeep.

expect JsonValue.to_str(Object([{ key: "a", value: Number("1") }, { key: "b", value: Array([Bool(True), Null]) }])) == Ok("{\"a\":1,\"b\":[true,null]}")

expect JsonValue.to_str(Number("NaN")) == Err(InvalidNumber("NaN"))
to_str_with : JsonValue, WriteOptions -> Try(Str, [InvalidNumber(Str), DuplicateKey(Str), TooDeep, TooLarge(U64)])

Write a value with the given JsonValue.WriteOptions. max_bytes limits UTF-8 output bytes, including escapes and indentation, checked before allocating output. It defaults to unlimited. Exceeding the budget is TooLarge(max_bytes). No final newline is added; reserve space in the budget when the calling protocol appends one.

expect JsonValue.to_str_with(Array([Number("1"), String("two")]), { indent: 4 }) == Ok("[\n    1,\n    \"two\"\n]")
Error : { line : U64, column : U64, message : Str }

Location and explanation of invalid JSON input. Lines and columns are one-based; a column counts bytes from the start of its line.

Options

:= {
    max_bytes : U64 ?? U64.highest,
    max_depth : U64 ?? 128,
    max_items : U64 ?? U64.highest,
    max_string_bytes : U64 ?? U64.highest,
    max_number_digits : U64 ?? U64.highest,
    comments : Bool ?? False,
    trailing_commas : Bool ?? False,
    bom : Bool ?? False,
    non_finite : Bool ?? False,
    control_characters : Bool ?? False,
    lone_surrogates : [Reject, Replace] ?? Reject,
    duplicates : [Reject, CollapseIdentical, KeepFirst, KeepLast] ?? Reject,
    top_level : [Any, ObjectOrArray, Object] ?? Any,
    interoperable_numbers : Bool ?? False,
}

How JsonValue.parse_with reads a document. Omitted fields use the defaults shown, which together are strict RFC 8259.

Limits:

  • max_bytes: bytes in the supplied input, including whitespace and any BOM, checked before constructing a value; unlimited by default;
  • max_depth: the most arrays and objects that may be nested;
  • max_items: the most items in one array or members in one object;
  • max_string_bytes: the most UTF-8 bytes in one decoded string or key;
  • max_number_digits: the most characters in one number.

Relaxations, each off by default:

  • comments: // line comments and /* */ block comments wherever whitespace may appear;
  • trailing_commas: a comma after the last item or member;
  • bom: skip a leading byte order mark (U+FEFF);
  • non_finite: NaN, Infinity and -Infinity, kept as Number text;
  • control_characters: unescaped U+0000 to U+001F inside strings;
  • lone_surrogates: Replace turns an unpaired \u surrogate into U+FFFD instead of rejecting it.

Checks:

  • duplicates: what a key repeated in one object means. Keys are compared after unescaping. Reject refuses it. CollapseIdentical keeps one copy of a key repeated with an equal value and refuses one repeated with a different value. KeepFirst ignores later copies. KeepLast keeps the first position with the last value, as JavaScript and Python do;
  • top_level: require the document to be an Object, or an ObjectOrArray;
  • interoperable_numbers: refuse numbers that an IEEE 754 double cannot hold, as I-JSON (RFC 7493) requires: integers beyond ±(2^53−1), more than 17 significant digits, and values that overflow or underflow.
expect JsonValue.parse_with("[1, 2,] // done", { comments: True, trailing_commas: True }) == Ok(Array([Number("1"), Number("2")]))

WriteOptions

:= { sort_keys : Bool ?? False, indent : U64 ?? 0, max_bytes : U64 ?? U64.highest }

How JsonValue.to_str_with writes a value. sort_keys writes object members in byte order of their keys, recursively. indent greater than zero writes each item and member on its own line, indented by that many spaces per level. max_bytes bounds the serialized UTF-8 bytes, including escapes and indentation, and defaults to unlimited.

expect JsonValue.to_str_with(Object([{ key: "b", value: Null }, { key: "a", value: Array([]) }]), { sort_keys: True, indent: 2 }) == Ok("{\n  \"a\": [],\n  \"b\": null\n}")