Skip to content

extractKeyMap doesn't follow $ref / recursive schemas — unbounded-depth trees silently lose keying #29

Description

@maxjay

Problem

extractKeyMap (src/engine.ts) builds the pattern→key map by walking the schema's literal properties and items only. It does not resolve $ref or follow recursive schema definitions. So for a recursively-shaped tree — a node whose children array has the same keyed shape as the node — keying is only registered as deep as the schema is written out literally. Past that depth, arrays fall back to index-zip diffing and items()/diff() lose identity addressing silently (no error, just a wrong/cascading diff).

Example

A natural way to express an arbitrary-depth tree is a $ref:

{
  "type": "object",
  "properties": {
    "nodes": { "type": "array", "x-key": "id", "items": { "$ref": "#/$defs/node" } }
  },
  "$defs": {
    "node": {
      "type": "object",
      "properties": {
        "children": { "type": "array", "x-key": "id", "items": { "$ref": "#/$defs/node" } }
      }
    }
  }
}

extractKeyMap registers nothing for children (it never enters the $ref), so editing a grandchild produces an index-zip cascade instead of one identity-attributed op.

Today you must either:

  • write x-key literally as deep as you ever nest ($['nodes'][*]['children'][*]['children'][*]…), which is impossible for unbounded depth; or
  • pass { key } inline on each items() / diff() call at the level you're reading (this works, because inline key resolves against the concrete path at call time).

Possible directions

  • Resolve $ref against $defs/# when walking the schema (handle self-reference without infinite recursion — detect cycles and register the key pattern as repeating).
  • Or: support a recursive/wildcard pattern entry in the keyMap (e.g. a pattern that matches children at any depth) so a self-referential shape needs one registration.
  • At minimum: document the limitation and the inline-key workaround in docs/engine.md.

Origin: surfaced while building a recursive tree editor on items() (branch claude/practical-shannon-jr19ie). Deliberately not addressed there — items()/diff() work fine with inline { key } per call.

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

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions