JSON and JSON5 configuration

The config/formats/json5 package (@json5) parses .json and .json5 files with one JSON5 parser, so JSON files can hold comments. Import moonrockz/moon-lander/config/formats/json5 as @json5. Each mbt check block on this page runs as a test with mise run test.

///|
test "parse JSON5 with a comment" {
  let text =
    #|{ // layout
    #|  "ir": { "layout": "tree" }
    #|}
  let value = @json5.parse(text, origin="app.json")
  @expect.expect(@config.get_text(
    @config.build([@config.in_memory(value)]),
    "ir.layout",
  )).to_equal("tree")
}

Number precision

moonbitlang/x/json5 gives every number as a Double, with no source text. An integral number whose magnitude is at most 2^53 (9_007_199_254_740_992) becomes an Ion integer. Every other number, including Infinity and NaN, becomes a float. Integers above 2^53 lose precision with no error. Prefer TOML or Ion, or a string, when a value must stay exact beyond that range.

///|
test "safe integers stay exact; larger magnitudes become floats" {
  let safe = @json5.parse(
    "{\"n\": 9007199254740992}",
    origin="app.json",
  )
  guard safe.to_ion().kind() is Struct(fields) else { fail("expected object") }
  guard fields[0].value().kind() is Int(_) else {
    fail("2^53 should stay an integer")
  }
  // Magnitudes above 2^53 cannot be exact in Double; 1e20 is still integral as
  // a Double but becomes a float under the configuration number rule.
  let large = @json5.parse("{\"n\": 1e20}", origin="app.json")
  guard large.to_ion().kind() is Struct(fields) else { fail("expected object") }
  guard fields[0].value().kind() is Float(_) else {
    fail("above 2^53 should become a float")
  }
}

Duplicate keys

The JSON5 parser keeps no duplicate-key information: when a key occurs more than once in one object, the last value wins. TOML, YAML, and Ion report duplicate keys as configuration errors. Do not rely on duplicate detection for JSON or JSON5 sources.

///|
test "duplicate JSON keys keep the last value" {
  let value = @json5.parse("{\"a\": 1, \"a\": 2}", origin="app.json")
  @expect.expect(value).to_equal(
    @config.fields([("a", @config.int(2))]),
  )
}

View source on GitHub