use serde::Serialize;
use serde_json::Value;
pub const VERSION: u16 = 1;
pub fn to_bytes<T: Serialize + ?Sized>(value: &T) -> Result<Vec<u8>, serde_json::Error> {
let value = serde_json::to_value(value)?;
Ok(value_bytes(&value))
}
#[must_use]
pub fn value_bytes(value: &Value) -> Vec<u8> {
let mut out = Vec::new();
write_canonical(value, &mut out);
out
}
#[must_use]
pub fn first_difference(left: &Value, right: &Value) -> Option<String> {
fn escape(token: &str) -> String {
token.replace('~', "~0").replace('/', "~1")
}
fn walk(left: &Value, right: &Value, at: &str) -> Option<String> {
match (left, right) {
(Value::Object(l), Value::Object(r)) => {
let mut keys: Vec<&String> = l.keys().chain(r.keys()).collect();
keys.sort_by(|a, b| utf16_order(a, b));
keys.dedup();
for key in keys {
let path = format!("{at}/{}", escape(key));
match (l.get(key), r.get(key)) {
(Some(lv), Some(rv)) => {
if let Some(found) = walk(lv, rv, &path) {
return Some(found);
}
}
_ => return Some(path),
}
}
None
}
(Value::Array(l), Value::Array(r)) => {
for (index, (lv, rv)) in l.iter().zip(r.iter()).enumerate() {
if let Some(found) = walk(lv, rv, &format!("{at}/{index}")) {
return Some(found);
}
}
(l.len() != r.len()).then(|| format!("{at}/{}", l.len().min(r.len())))
}
_ => (value_bytes(left) != value_bytes(right)).then(|| at.to_owned()),
}
}
walk(left, right, "")
}
fn utf16_order(a: &str, b: &str) -> std::cmp::Ordering {
a.encode_utf16().cmp(b.encode_utf16())
}
fn write_canonical(value: &Value, out: &mut Vec<u8>) {
match value {
Value::Object(map) => {
out.push(b'{');
let mut keys: Vec<&str> = map.keys().map(String::as_str).collect();
keys.sort_unstable_by(|a, b| utf16_order(a, b));
for (i, key) in keys.iter().enumerate() {
if i > 0 {
out.push(b',');
}
write_scalar(&Value::String((*key).to_owned()), out);
out.push(b':');
write_canonical(&map[*key], out);
}
out.push(b'}');
}
Value::Array(items) => {
out.push(b'[');
for (i, item) in items.iter().enumerate() {
if i > 0 {
out.push(b',');
}
write_canonical(item, out);
}
out.push(b']');
}
scalar => write_scalar(scalar, out),
}
}
fn write_scalar(value: &Value, out: &mut Vec<u8>) {
if let Value::Number(n) = value
&& n.is_f64()
{
es_number(n.as_f64().expect("is_f64 implies as_f64"), out);
return;
}
serde_json::to_writer(&mut *out, value).expect("serde_json cannot fail on a scalar Value");
}
fn es_number(value: f64, out: &mut Vec<u8>) {
if value == 0.0 {
out.push(b'0');
return;
}
if value.is_sign_negative() {
out.push(b'-');
es_number(-value, out);
return;
}
let sci = format!("{value:e}");
let (mantissa, exponent) = sci
.split_once('e')
.expect("LowerExp always writes an exponent");
let digits: Vec<u8> = mantissa.bytes().filter(|b| *b != b'.').collect();
let exponent: i32 = exponent.parse().expect("a LowerExp exponent is an integer");
let k = i32::try_from(digits.len()).expect("shortest f64 digits fit in i32");
let n = exponent + 1;
if n >= k && n <= 21 {
out.extend_from_slice(&digits);
out.extend(std::iter::repeat_n(
b'0',
usize::try_from(n - k).expect("n >= k"),
));
} else if n > 0 && n <= 21 {
let split = usize::try_from(n).expect("n > 0");
out.extend_from_slice(&digits[..split]);
out.push(b'.');
out.extend_from_slice(&digits[split..]);
} else if n > -6 && n <= 0 {
out.extend_from_slice(b"0.");
out.extend(std::iter::repeat_n(
b'0',
usize::try_from(-n).expect("n <= 0"),
));
out.extend_from_slice(&digits);
} else {
out.push(digits[0]);
if digits.len() > 1 {
out.push(b'.');
out.extend_from_slice(&digits[1..]);
}
out.push(b'e');
let e = n - 1;
out.push(if e < 0 { b'-' } else { b'+' });
out.extend_from_slice(e.unsigned_abs().to_string().as_bytes());
}
}
#[must_use]
pub fn json_kind(value: &Value) -> &'static str {
match value {
Value::Null => "null",
Value::Bool(_) => "a boolean",
Value::Number(_) => "a number",
Value::String(_) => "a string",
Value::Array(_) => "an array",
Value::Object(_) => "an object",
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn sorted_keys_guard() {
let a = json!({"z": 1, "a": 2, "m": 3});
let b = json!({"a": 2, "m": 3, "z": 1});
assert_eq!(
value_bytes(&a),
value_bytes(&b),
"serde_json must sort object keys — is `preserve_order` enabled?"
);
assert_eq!(
String::from_utf8(value_bytes(&a)).unwrap(),
r#"{"a":2,"m":3,"z":1}"#
);
}
#[test]
fn keys_sort_by_utf16_code_unit_not_utf8_byte() {
let bmp = "\u{ffff}";
let astral = "\u{10000}";
assert!(
bmp < astral,
"the fixture no longer distinguishes the orderings"
);
let bytes = value_bytes(&json!({ astral: 1, bmp: 2 }));
let text = String::from_utf8(bytes).unwrap();
assert!(
text.find(astral) < text.find(bmp),
"keys came out in UTF-8 byte order, not UTF-16 — so a signed Agent \
Card canonicalized here is rejected by any conforming verifier, and \
every ASCII test still passes: {text}"
);
}
#[test]
fn nested_objects_are_also_sorted() {
let a = json!({"outer": {"z": 1, "a": 2}});
let b = json!({"outer": {"a": 2, "z": 1}});
assert_eq!(value_bytes(&a), value_bytes(&b));
}
#[test]
fn array_order_is_significant() {
assert_ne!(value_bytes(&json!([1, 2])), value_bytes(&json!([2, 1])));
}
#[expect(
clippy::excessive_precision,
reason = "the literal is RFC 8785's own vector, quoted verbatim"
)]
#[test]
fn doubles_format_per_rfc_8785() {
let vectors: &[(f64, &str)] = &[
(0.0, "0"),
(-0.0, "0"), (4.5, "4.5"),
(0.002, "0.002"),
(1e-6, "0.000001"), (1e-7, "1e-7"), (1e20, "100000000000000000000"), (1e21, "1e+21"), (1e30, "1e+30"),
(1e-27, "1e-27"),
(9_007_199_254_740_992.0, "9007199254740992"), (333_333_333.333_333_29, "333333333.3333333"), (9.999_999_999_999_997e22, "9.999999999999997e+22"),
(5e-324, "5e-324"), (1.797_693_134_862_315_7e308, "1.7976931348623157e+308"), (-4.5, "-4.5"),
(-1e30, "-1e+30"),
];
for (input, expected) in vectors {
let mut out = Vec::new();
es_number(*input, &mut out);
assert_eq!(
std::str::from_utf8(&out).unwrap(),
*expected,
"RFC 8785 formats {input:?} as {expected}"
);
}
}
#[test]
fn canonical_bytes_carry_rfc_8785_numbers() {
let value = json!({
"big": 1e30,
"frac": 4.5,
"int": 100,
"neg": -0.0,
"small": 1e-7,
"whole": 100.0,
});
assert_eq!(
String::from_utf8(value_bytes(&value)).unwrap(),
r#"{"big":1e+30,"frac":4.5,"int":100,"neg":0,"small":1e-7,"whole":100}"#
);
}
#[test]
fn integers_beyond_double_precision_stay_distinct() {
let a = json!(9_007_199_254_740_993_u64); let b = json!(9_007_199_254_740_992_u64); assert_ne!(value_bytes(&a), value_bytes(&b));
assert_eq!(
String::from_utf8(value_bytes(&a)).unwrap(),
"9007199254740993"
);
}
#[test]
fn equal_values_have_no_first_difference() {
let a = serde_json::json!({ "b": 1, "a": [1, 2, { "x": true }] });
let b = serde_json::json!({ "a": [1, 2, { "x": true }], "b": 1 });
assert_eq!(first_difference(&a, &b), None);
}
#[test]
fn a_differing_leaf_is_named_by_its_path() {
let a = serde_json::json!({ "to": "GB", "amount": 12000 });
let b = serde_json::json!({ "to": "GB", "amount": 999 });
assert_eq!(first_difference(&a, &b).as_deref(), Some("/amount"));
}
#[test]
fn a_root_level_difference_is_the_empty_pointer() {
let a = serde_json::json!(null);
let b = serde_json::json!({ "to": "GB" });
assert_eq!(first_difference(&a, &b).as_deref(), Some(""));
}
#[test]
fn a_missing_member_is_named_rather_than_its_parent() {
let a = serde_json::json!({ "outer": { "kept": 1 } });
let b = serde_json::json!({ "outer": { "kept": 1, "added": 2 } });
assert_eq!(first_difference(&a, &b).as_deref(), Some("/outer/added"));
}
#[test]
fn a_shorter_array_is_named_at_the_first_index_it_lacks() {
let a = serde_json::json!({ "xs": [1, 2] });
let b = serde_json::json!({ "xs": [1, 2, 3] });
assert_eq!(first_difference(&a, &b).as_deref(), Some("/xs/2"));
}
#[test]
fn pointer_tokens_are_escaped() {
let a = serde_json::json!({ "a/b": 1, "c~d": 1 });
let b = serde_json::json!({ "a/b": 2, "c~d": 1 });
assert_eq!(first_difference(&a, &b).as_deref(), Some("/a~1b"));
let c = serde_json::json!({ "a/b": 1, "c~d": 2 });
assert_eq!(first_difference(&a, &c).as_deref(), Some("/c~0d"));
}
#[test]
fn the_pointer_agrees_with_canonical_equality() {
let a: Value = serde_json::from_str("{\"n\": 1.0}").unwrap();
let b: Value = serde_json::from_str("{\"n\": 1}").unwrap();
assert_eq!(value_bytes(&a), value_bytes(&b));
assert_eq!(first_difference(&a, &b), None);
}
#[test]
fn the_first_difference_is_reported_in_canonical_key_order() {
let a = serde_json::json!({ "z": 1, "a": 1 });
let b = serde_json::json!({ "z": 2, "a": 2 });
assert_eq!(
first_difference(&a, &b).as_deref(),
Some("/a"),
"keys are visited in the order canonical bytes write them"
);
}
}