Skip to main content

jstrict

jstrict parses JSON exactly as RFC 8259 and ECMA-404 define it, and keeps what most parsers drop: the order of an object's members, a name that appears twice and the way each number was written. Parsing also records where in the text every value came from. It is a fork of json-syntax by Timothée Haudebourg, with the upstream history kept, and its number module is his json-number. jsonld holds every document it processes as a jstrict value.

What JSON leaves open​

RFC 8259 calls an object an unordered collection (section 1) and says the names in it SHOULD be unique (section 4). When they are not, what the receiver does is unpredictable, and many implementations keep only the last pair. ECMA-404 section 6 gives neither the uniqueness nor the order of names any meaning, and leaves both to the specifications built on JSON.

A parser that keeps one value per name, or sorts the names, has made that choice for the specification above it. JSON-LD 1.1, for one, requires unique keys (section 9), and a processor can only refuse a repeated key it is shown. jstrict decides nothing and hands over the document as written.

Objects, numbers and positions​

  • An object is a list of entries in the order written, with an index for lookup. get returns every entry with a name, and get_unique fails with Duplicate when there is more than one. insert replaces the first match and removes the rest, and push appends. Equality respects order, and as_unordered compares without it.
  • A number keeps its written form, so 1E30 prints back as 1E30. as_i64 and as_u64 check the range, as_f64_lossy may round, and as_f64_lossless returns None when the value would not round-trip.
  • Parsing returns a code map beside the value, with one flat entry per fragment (a value, an entry or a key) in depth-first order. Each entry has a byte span and counts the fragments it contains, so a subtree can be skipped in one step. The value holds nothing about the source text, and parse_str_value skips the code map altogether.
use jstrict::{Parse, Print, Value};

fn main() -> Result<(), Box<dyn std::error::Error>> {
let text = r#"{"b": 1E30, "a": 1, "a": 2}"#;
let (value, code_map) = Value::parse_str(text)?;
let object = value.as_object().ok_or("not an object")?;

assert_eq!(object.get("a").count(), 2);
assert!(object.get_unique("a").is_err());
assert_eq!(value.compact_print().to_string(), r#"{"b":1E30,"a":1,"a":2}"#);

// Fragment 3 is the value of "b".
let span = code_map[3].span;
assert_eq!(&text[span.start..span.end], "1E30");
Ok(())
}

Speed​

  • The parser reads a byte slice. It scans strings and numbers byte by byte, copies a run without escapes as one slice, and matches null, true and false with one comparison each.
  • Control and escape bytes are found eight bytes at a time, and memchr finds the next " or \ in a string.
  • UTF-8 is validated with SIMD through simdutf8, a default feature, and strings are escaped with SIMD when printed.
  • The parser is generic over what records spans, so the path without a code map is compiled without span recording, not branched around it.
  • The key index of an object caches the hash of each key, so the index grows without hashing anything again.

Depth​

Parsing and dropping a value of any depth use heap memory, not the call stack. Printing and serialising with serde run each level of nesting on enough native stack, switching to a fresh stack segment when the current one runs short.

Canonical form​

With the canonicalize feature, canonicalize rewrites a value into the JSON Canonicalization Scheme of RFC 8785. Keys are sorted by their UTF-16 code units (section 3.2.3) and numbers take the shortest form that reads back as the same double (section 3.2.2.3), so the value printed compactly is the canonical text. canonicalize_with reuses a caller's number buffer across calls.

When upstream fits better​

If your code already holds json-syntax values, as upstream json-ld does, stay with json-syntax. The two read JSON the same way, and mixing them adds a conversion wherever they meet.

Where Andromeda uses it​

Andromeda doesn't read request bodies with jstrict. A body is decoded straight into the typed model, which refuses a member named twice (Request bodies). jstrict holds only the JSON the broker exchanges with the JSON-LD processor. The broker hands the processor text that it reads with parse_str_value, since nothing there needs positions, and it puts the members of every compacted answer in key order with a loop rather than recursion, so ordering is safe at any depth.

A check confines jstrict to the JSON-LD engine and the type that holds JSON literals (Three forms of JSON). Standards lists the JSON specifications Andromeda follows.