On this page· 4

Identity and order

  • file format 6
  • Rust SDK 1.0 preview

Merge, diff and the writer's sort order all rest on a few values every record has: its kind, its rank, its primary id, and two keys built from them.

The values

Table 1. Identity values: 5
ValueDefinitionRust
kindthe k stringRecord::kind
rankthe registry rank of a typed kind; any other kind ranks after all typed kindsfirst element of Record::sort_key
primary idfor a typed record, the id its kind defines. For an opaque record, the first of id, ref_id, session_id, answer_id, clause_id that is a string; if none is, the identity form of the whole recordRecord::primary_id
dedup key(kind, primary id). Two records with the same dedup key are the same record.Record::dedup_key
sort key(rank, kind, primary id). Rank compares as a number; kind and primary id compare by their UTF-8 bytes.Record::sort_key

A writer emits records in ascending sort-key order. Records with equal sort keys keep the order they were given in.

A writer should not emit two records with one dedup key. A reader accepts such a file, and diff lists the key under duplicate_keys.

An opaque record whose kind is a typed kind at a higher v has the same dedup key as the version-1 record with the same primary id. Merge treats the two as one record.

Needs the Rust SDK

This program also needs serde_json = "1.0" under [dependencies] in its Cargo.toml.

identity.rsrust
// Identity: the canonical JSON form, and the keys that decide whether
// two records are "the same record".
use plxi_sdk::{canonical_json, Record};

fn main() -> Result<(), plxi_sdk::Error> {
    // canonical_json sorts object members by name, keeps array order
    // and removes insignificant whitespace.
    let value = serde_json::json!({
        "z": [3, 1.5, -0.0],
        "a": {"y": null, "b": true}
    });
    let canon = r#"{"a":{"b":true,"y":null},"z":[3,1.5,-0.0]}"#;
    assert_eq!(canonical_json(&value), canon);

    // A typed record's key is (kind, primary id).
    let line = r#"{"k":"ent","id":"e1","t":"Person"}"#;
    let ent = Record::from_json_line(line)?;
    let parts = (ent.kind(), ent.version(), ent.primary_id());
    assert_eq!(parts, ("ent", 1, "e1"));
    assert_eq!(ent.dedup_key(), ("ent".to_string(), "e1".to_string()));
    assert_eq!(ent.sort_key(), (3, "ent".to_string(), "e1".to_string()));

    // A kind this SDK has no schema for is kept as written ("opaque"),
    // so a newer writer's records survive a round trip through an
    // older reader.
    let line = r#"{"k":"x-future","v":7,"id":"n1","z":{"b":1,"a":2}}"#;
    let opaque = Record::from_json_line(line)?;
    assert_eq!(opaque.json_line(), line);
    let canon = r#"{"id":"n1","k":"x-future","v":7,"z":{"a":2,"b":1}}"#;
    assert_eq!(opaque.canonical_json(), canon);
    assert_eq!(opaque.sort_key().0, u32::MAX); // unknown kinds sort last

    // Two spellings of one object are one record: an opaque record
    // with no id member is keyed by its canonical form.
    let a = r#"{"k":"x-note","b":1,"a":{"y":2.0,"x":1}}"#;
    let b = r#"{"a":{"x":1,"y":2.00},"b":1,"k":"x-note"}"#;
    let a = Record::from_json_line(a)?;
    let b = Record::from_json_line(b)?;
    assert_ne!(a.json_line(), b.json_line());
    assert_eq!(a.dedup_key(), b.dedup_key());
    println!("{}", a.primary_id());
    Ok(())
}
Output
{"a":{"x":1,"y":2.0},"b":1,"k":"x-note"}

Ran with cargo run --example 07_identity · exit status 0

The last line printed is a primary id. The record has none of the five id members, so its primary id is the identity form of the whole record, and two spellings of it share one key.

The identity form

The identity form of a JSON value is the value written with object members sorted by name at every depth, array order unchanged, and no whitespace between tokens. Names sort by their UTF-8 bytes. The SDK calls it canonical JSON.

It is used for the fallback primary id above, to decide whether a record changed in a diff, and for the expected values of the conformance cases.

canonical_json returns it for any serde_json::Value; Record::canonical_json returns it for a record.

How numbers are written

A number written as an integer that fits a signed or unsigned 64-bit integer is written back exactly. Every other number is a binary64 value, the 64-bit floating-point type of IEEE 754, written as the shortest decimal string that reads back to the same value.

Table 2. Number spelling: 6 examples
ReadWrittenRule
1E2100.0a binary64 value with no fraction keeps .0
-0.0-0.0negative zero is kept
0.000010.00001positional from 1e-5 up to, not including, 1e16
1e161e+16exponent form outside that range
2.002.0shortest form
1844674407370955161518446744073709551615a 64-bit integer stays exact

When two shortest strings read back to the same value, the spelling is the one serde_json 1.0.149 writes. The binary64 value 0x42e87faaebb9a0d4 reads back from both 215492859907334.62 and 215492859907334.63; it is written 215492859907334.62.

A reader returns the binary64 value nearest the number it reads, with ties to even. Under these two rules, reading an identity form and writing it again gives the same bytes.

An implementation in any language writes numbers by these rules, not with its language's default formatter. JavaScript's JSON.stringify writes 1.0 as 1 and -0.0 as 0; Python's json.dumps writes 0.00001 as 1e-05. The conformance corpus has a numbers case that checks these spellings. Spec §10.2 · Spec §10.4

This is not RFC 8785

RFC 8785, the JSON Canonicalization Scheme, is a different canonical form. An implementation does not use a library for it in place of the identity form. The two differ in seven ways.

Table 3. Identity form and RFC 8785: 7 differences
ReadIdentity formRFC 8785Rule that differs
member names "דּ" and "😀"U+FB33 first (UTF-8 EF.. < F0..)U+1F600 first (UTF-16 D83D < FB33)key order: UTF-8 bytes vs UTF-16 code units (RFC 8785 §3.2.3)
1.0, 100.0, 1E21.0, 100.0, 100.01, 100, 100integral binary64 keeps .0
-0.0-0.00negative zero
18446744073709551615184467440737095516151844674407370955200064-bit integers stay exact
1e-61e-60.000001positional range lower bound (1e-5 here, 1e-6 in ECMAScript)
1e16, 1e171e+16, 1e+1710000000000000000, 100000000000000000positional range upper bound (1e16 here, 1e21 in ECMAScript)
184467440737095516161.8446744073709552e+1918446744073709552000same binary64 value, different spelling

Strings, the literals true, false and null, and the absence of whitespace agree.

Sections