mirror: A fast canonicalisation utility for stable object equality
0

Configure Feed

Select the types of activity you want to include in your feed.

feat: objects with toJSON uses that for its identity

Objects with a `toJSON` method (URL, Temporal, custom types) now serialize
as whatever it returns.

+42B, perf-neutral on realistic shapes.

Marais Rossouw (Jun 25, 2026, 3:42 PM +1000) 0ff606e7 5e8d86a4

+40 -3
+34 -2
lib/mod.test.ts
··· 101 101 assertEquals(identify(() => {}), identify(undefined)); 102 102 }); 103 103 104 + // ~> toJSON 105 + 106 + Deno.test('toJSON :: result is serialized in place', () => { 107 + assertEquals(identify({ toJSON: () => ({ a: 1, b: [2] }) }), identify({ a: 1, b: [2] })); 108 + assertEquals(identify({ ignored: 99, toJSON: () => ({ a: 1 }) }), identify({ a: 1 })); 109 + // Primitive and undefined results behave like that primitive / a drop. 110 + assertEquals(identify({ toJSON: () => 42 }), identify(42)); 111 + assertEquals(identify({ toJSON: () => 'x' }), identify('x')); 112 + assertEquals(identify({ a: 1, b: { toJSON: () => undefined } }), identify({ a: 1 })); 113 + }); 114 + 115 + Deno.test('toJSON :: URL serializes as its href', () => { 116 + assertEquals( 117 + identify(new URL('https://example.com/a?b=1#c')), 118 + identify('https://example.com/a?b=1#c'), 119 + ); 120 + assertEquals(identify(new URL('https://a.test/')), identify(new URL('https://a.test/'))); 121 + assertNotEquals(identify(new URL('https://a.test/')), identify(new URL('https://b.test/'))); 122 + }); 123 + 124 + Deno.test('toJSON :: builtins with special forms ignore toJSON', () => { 125 + assertEquals(identify(new Date(0)), 'd0'); 126 + assertNotEquals(identify(new Date(0)), identify(new Date(0).toJSON())); 127 + assertEquals(identify(Buffer.from([1, 2])), identify(new Uint8Array([1, 2]))); 128 + }); 129 + 130 + Deno.test('toJSON :: only applied once, like JSON.stringify', () => { 131 + const o: any = { a: 1 }; 132 + o.toJSON = () => o; 133 + assertEquals(identify(o), identify({ a: 1 })); 134 + }); 135 + 104 136 // ~> Dates & RegExps 105 137 106 138 Deno.test('dates :: equal instants match, different instants differ', () => { ··· 489 521 assertWire('uint8 clamped array', new Uint8ClampedArray([1, 255]), 'o0n11n255'); 490 522 assertWire('uint16 array', new Uint16Array([1, 2]), 'o0n11n2'); 491 523 assertWire('uint32 array', new Uint32Array([1, 2]), 'o0n11n2'); 492 - //assertWire('custom toJSON', { toJSON: () => ({ a: 1, b: [2] }) }, 'oan1ban2'); 493 - //assertWire('url', new URL('https://example.com/a?b=1#c'), 'shttps://example.com/a?b=1#c'); 524 + assertWire('custom toJSON', { toJSON: () => ({ a: 1, b: [2] }) }, 'oan1ban2'); 525 + assertWire('url', new URL('https://example.com/a?b=1#c'), 'shttps://example.com/a?b=1#c'); 494 526 }); 495 527 496 528 Deno.test('format :: circular array back-reference', () => {
+5
lib/mod.ts
··· 17 17 if (!is_arr) { 18 18 if (input instanceof Date) return 'd' + +input; 19 19 if (input instanceof RegExp) return 'r' + input.source + input.flags; 20 + if (typeof input.toJSON === 'function' && !ArrayBuffer.isView(input)) { 21 + input = input.toJSON(); 22 + if (typeof input !== 'object' || input === null) return walk(input, seen); 23 + is_arr = Array.isArray(input); 24 + } 20 25 } 21 26 22 27 let ref: any = seen.indexOf(input);
+1 -1
readme.md
··· 28 28 - 🧬 **Canonical.** The same shape always produces the same id. 29 29 - 🌀 **Deep and cycle-safe.** Handles nested objects, arrays, sets, maps, and circular references. 30 30 - 🏎 **Fast.** See the [benchmarks](#-benchmark). 31 - - 🪶 **Tiny.** Around 529B minified and gzipped, with zero 31 + - 🪶 **Tiny.** Around 571B minified and gzipped, with zero 32 32 [dependencies](https://npm.anvaka.com/#/view/2d/object-identity/). 33 33 34 34 ## ⚙️ Install