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.
getreturns every entry with a name, andget_uniquefails withDuplicatewhen there is more than one.insertreplaces the first match and removes the rest, andpushappends. Equality respects order, andas_unorderedcompares without it. - A number keeps its written form, so
1E30prints back as1E30.as_i64andas_u64check the range,as_f64_lossymay round, andas_f64_losslessreturnsNonewhen 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_valueskips 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,trueandfalsewith one comparison each. - Control and escape bytes are found eight bytes at a time, and
memchrfinds 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.