JSON Canonicalizer For Stable Signing

Serialize JSON with sorted keys, strict numeric checks and cycle rejection so independent services compute identical signing payloads.

TypescriptData Integrity

Code

snippet
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");
}