prisma/prisma · error · TypeError

canonicalStringify: objects with symbol-keyed properties are

Error message

canonicalStringify: objects with symbol-keyed properties are not supported

What it means

Thrown by canonicalStringify's writePlainObject when a plain object has one or more symbol-keyed own properties (detected via Object.getOwnPropertySymbols). Object.keys ignores symbol keys, so they would be silently dropped from the canonical form, producing a key that omits real data. The function refuses to produce a lossy key.

Source

Thrown at packages/1-framework/0-foundation/utils/src/canonical-stringify.ts:138

}

function writePlainObject(obj: Record<string, unknown>, seen: Set<object>): string {
  // Only true plain objects are accepted here. Without this guard, anything
  // that fell through the type-tagged branches above (`Map`, `Set`,
  // `RegExp`, class instances, …) would canonicalize to `{}` because
  // `Object.keys` returns no enumerable string keys for them — silently
  // colliding with each other and with the literal `{}`.
  const proto = Object.getPrototypeOf(obj);
  if (proto !== Object.prototype && proto !== null) {
    const tag = proto?.constructor?.name ?? 'unknown';
    throw new TypeError(`canonicalStringify: non-plain objects are not supported (got ${tag})`);
  }

  // `Object.keys` ignores symbol-keyed properties, so they would be
  // silently dropped from the canonical form. Force callers to handle
  // them explicitly instead of producing a key that omits real data.
  if (Object.getOwnPropertySymbols(obj).length > 0) {
    throw new TypeError(
      'canonicalStringify: objects with symbol-keyed properties are not supported',
    );
  }

  const keys = Object.keys(obj).sort();
  const parts: string[] = [];
  for (const key of keys) {
    parts.push(`${JSON.stringify(key)}:${write(obj[key], seen)}`);
  }
  return `{${parts.join(',')}}`;
}

function bytesToHex(bytes: Uint8Array): string {
  let out = '';
  for (let i = 0; i < bytes.length; i++) {
    const byte = bytes[i] as number;
    out += byte.toString(16).padStart(2, '0');
  }

View on GitHub (pinned to a20d61fb6f)

Solutions

  1. Project the object to a new plain object containing only the string-keyed properties that form its identity.
  2. Explicitly convert symbol-keyed properties to string keys using a deterministic mapping (e.g. Symbol.keyFor).
  3. Strip symbol properties before canonicalizing if they are not part of the identity.

Example fix

// before
canonicalStringify({ a: 1, [Symbol('s')]: 2 })
// after
canonicalStringify({ a: 1 })
Defensive patterns

Strategy: validation

Validate before calling

function hasSymbolKeys(v: unknown): boolean {
  if (v && typeof v === 'object') {
    if (Object.getOwnPropertySymbols(v).length > 0) return true;
    return Object.values(v).some(hasSymbolKeys);
  }
  return false;
}

Type guard

function hasNoSymbolKeys(o: Record<string, unknown>): boolean {
  return Object.getOwnPropertySymbols(o).length === 0;
}

Try / catch

try {
  canonicalStringify(value);
} catch (e) {
  if (e instanceof TypeError && /symbol-keyed properties/.test(e.message)) {
    // strip or stringify symbol keys and retry
  }
  throw e;
}

Prevention

When it happens

Trigger: Passing a plain object that has at least one Symbol-typed key, e.g. { [Symbol('x')]: 1, a: 2 } or an object extended with symbol properties by a library.

Common situations: An ORM/library decorates objects with symbol-keyed metadata; using Symbol.for() as a property key for private-ish fields; caching a record that picked up symbol properties through spread/assign.

Related errors


AI-assisted analysis of prisma/prisma@a20d61fb6f (2026-08-11). Data as JSON: /api/errors/5ea7762495bd207a. Report an issue: GitHub.