功能说明
该示例生成确定性的 JSON 文本:对象键按 UTF-16 顺序排序,数组保持原顺序,字符串与数字采用 ECMAScript 的 JSON 表示。相同语义的数据无论属性插入顺序如何,都会得到完全相同的字符串,适合作为 HMAC 签名、幂等指纹、缓存键或内容哈希的输入。
关键代码
序列化函数用栈集合检测循环引用,进入数组或对象时加入、完成时移除。对象只接受普通对象或 null 原型对象,拒绝类实例、Map 等无法用 JSON 表达的值。键排序使用默认的 sort,即按 UTF-16 码元比较,与常见 JCS 实现的排序规则一致。数字通过 JSON.stringify 输出,非有限值直接拒绝,数组元素与对象键值都递归处理,不忽略 undefined。
使用注意
该实现是 RFC 8785 思路的简化版,未处理全部 Unicode 转义与数字格式细节;与第三方 JCS 库互通前应做交叉测试。undefined 在标准 JSON.stringify 中会被省略或变成 null,这里选择直接抛错,避免签名双方对缺失字段理解不一致。循环引用与访问器无法规范化。签名前还应明确字符编码为 UTF-8,并把结果按字节而不是字符串长度处理。
验证方式
对键顺序不同的两个对象断言输出相同;嵌套数组与对象比较输出;测试字符串含引号、换行与 emoji 时的转义;确认 NaN、Infinity、undefined、BigInt 与函数抛 TypeError;构造自引用对象与数组,断言因循环抛错;类实例与 Map 抛错;对同一输入多次调用结果完全一致。
代码示例
export function canonicalizeJson(value: unknown): string {
return serialize(value, new Set<object>());
}
function serialize(value: unknown, stack: Set<object>): string {
if (value === null) return "null";
const type = typeof value;
if (type === "string") return JSON.stringify(value) as string;
if (type === "boolean") return value ? "true" : "false";
if (type === "number") {
if (!Number.isFinite(value)) throw new TypeError("non-finite numbers are not allowed");
return JSON.stringify(value) as string;
}
if (type === "undefined" || type === "function" || type === "symbol" || type === "bigint") {
throw new TypeError("value of type " + type + " is not valid JSON");
}
if (Array.isArray(value)) {
if (stack.has(value)) throw new TypeError("cyclic array");
stack.add(value);
const items = value.map((item) => serialize(item, stack));
stack.delete(value);
return "[" + items.join(",") + "]";
}
if (type === "object") {
const object = value as Record<string, unknown>;
const prototype = Object.getPrototypeOf(object);
if (prototype !== Object.prototype && prototype !== null) {
throw new TypeError("only plain objects can be canonicalized");
}
if (stack.has(object)) throw new TypeError("cyclic object");
stack.add(object);
const keys = Object.keys(object).sort();
const parts = keys.map((key) =>
(JSON.stringify(key) as string) + ":" + serialize(object[key], stack));
stack.delete(object);
return "{" + parts.join(",") + "}";
}
throw new TypeError("unsupported value");
}