Skip to content

Extend x-key: '$self' to non-primitive items via structural identity #18

Description

@maxjay

Background

x-key: '$self' declares set semantics on an array — the item itself is its identity. It currently only works on primitive items (strings, numbers, booleans, null), because JS's native Set/Map use value equality for primitives and reference equality for everything else.

new Set([1, 1]).size           // 1
new Set([{a:1}, {a:1}]).size   // 2  — different references

So $self on an array of objects would produce nonsense diffs today, and the diff path throws (or should throw) when it encounters non-primitive items.

What this issue tracks

Lifting that restriction so $self works on any JSON value — primitives, objects, nested arrays. The natural use case is "a set of structurally-identical small records where no field is a natural key" (e.g. [lat, lng] pairs treated as a set of locations).

Options considered

Two approaches both require walking each item's structure once, because JS doesn't expose structural equality natively:

1. Canonical-JSON normalization as identity. Stringify items with sorted keys, recursively. Use the string as a Set/Map key. No collisions by construction; memory cost is O(size) per item.

2. Deep-equality linear scan. No intermediate representation. For each item in a, check if any item in b deep-equals it. O(N·M·size) but short-circuits on first mismatch and allocates nothing extra.

For the array sizes where $self realistically applies (tags, permissions, small sets), either is fine. Deep-equality is probably simpler; canonical-JSON wins for larger N+M.

Why we're not doing it now

  • The overwhelming majority of "set of objects" use cases have a natural ID field, in which case x-key: '<field>' is the right tool and already works.
  • Extending $self to objects adds structural-equality machinery (canonicalizer or deep-equal) that has to be maintained, with edge cases around number representation, key ordering, and any future widening of JsonValue (e.g. NaN, undefined).
  • The clean two-tool design — x-key: '<field>' for stable identity, $self for primitive sets — covers ~all real schemas without overlap.

When to revisit

  • A real schema lands where $self on objects would be meaningfully cleaner than adding an id field to the data.
  • Or: TC39 Records & Tuples ships, at which point new Set(records) Just Works with value equality and the whole extension becomes free.

Acceptance criteria (if/when implemented)

  • x-key: '$self' no longer throws on object items.
  • Diffing [{a:1}, {a:2}][{a:1}, {a:3}] emits one remove + one add (not nested replace).
  • Duplicate-collapse and reorder-invisibility behaviors of $self are preserved (set semantics).
  • Implementation choice (canonicalize vs deep-equal) documented in code with a comment explaining the trade-off.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions