Skip to content

Reject a $ref in place of a schema that leads to an array (an enum, a required list, an array document) instead of emitting a tuple - #886

Draft
claude[bot] wants to merge 1 commit into
masterfrom
claude/hearth-thread-sw6nzt
Draft

claude[bot] wants to merge 1 commit into
masterfrom
claude/hearth-thread-sw6nzt

Conversation

@claude

@claude claude Bot commented Sep 7, 2026 •

Copy link
Copy Markdown
Contributor

Output change: a schema with a $ref in a schema position that leads to a list (an enum, a required list, a file that is an array) now fails with an error where it compiled to a tuple of the list's members or to unknown; every other schema's output is byte-identical (corpus, conformance and snapshots unchanged).

A $ref that stands where a schema is expected has to lead to an object or a boolean. #877 made a $ref onto a string, a number, null or an empty file an error that names the target. An array slipped through that guard, because it took any object for a schema, and came out as a made-up type.

Before (master):

$ json2ts schema.json
export interface A {
  y?: ["metres", "feet"];
}

After:

$ json2ts schema.json
error: ReferenceError: $ref "#/definitions/unit/enum" at properties/y resolves to an array of 2 items, which is not a JSON Schema.
A $ref in place of a schema must lead to an object or a boolean.

for

{"title": "A", "type": "object",
 "definitions": {"unit": {"type": "string", "enum": ["metres", "feet"]}},
 "properties": {"y": {"$ref": "#/definitions/unit/enum"}}}

The same error, naming the file, for a pointer into another file ($ref "lists.json#/definitions/u/required" at properties/y resolves to an array of 1 item in /abs/lists.json, which is not a JSON Schema.) and for a file whose whole content is an array ($ref "arr.json" at properties/y resolves to /abs/arr.json, which is not a JSON Schema: it parses to an array of 2 items.), through a file that is itself a $ref to such a file, and at the root ({"$ref": "arr.json"}, {"title": "T", "$ref": "arr.json"}, {"$ref": "#/definitions/unit/enum", "definitions": …}), where the whole schema used to compile to unknown.

Decided here: a whole-document array $ref ({"$ref": "arr.json"}) is an error too. An array is not a JSON Schema in any draft, and the alternative was export type T = unknown with no word about why.

Still fine, and pinned by tests:

  • a $ref onto a list in a data position: enum: {"$ref": "#/definitions/unit/enum"}, examples, default, tsEnumNames, vendor keys;
  • a list of property names under dependencies (dependencies: {"b": {"$ref": "#/required"}});
  • a $ref standing in for the list of schemas an items (or extends) may hold (items: {"$ref": "#/definitions/pair/items"} still gives [] | [string] | [string, number, ...unknown[]]). A list that holds anything but objects and booleans is rejected there (items: {"$ref": "arr.json"} with [1, 2]).

Why

isSchema in the resolver accepted any non-null object. The ref-parser's onDereference hook therefore never collected an array as a suspect, the in-document fast path never reported where its replacements sat (it had no non-schema target to report), and the root's own $ref is reported to no hook at all: with a keyword beside it (a title, or the definitions a pointer into the same document leads through) the ref-parser merges the array's members into the root as keys 0, 1, …, which then reads as an object.

How

  • isSchema excludes arrays; describeNonSchemaTarget names one ("an array of 2 items").
  • onDereference collects every non-schema target, arrays included; the in-document path passes the holder and key to it, as the ref-parser does, and judges what it collected.
  • The root's $ref is looked up through the ref-parser's $refs.get right after every document is read and before dereferencing, and rejected there if it leads to an array (continueOnError is switched off for that one lookup, so a pointer the ref-parser cannot resolve is neither thrown nor recorded there; its own error follows as before).
  • misplacedTargets: an array where a single schema stands is misplaced; standing in for a whole items/extends value it is fine when every member is an object or boolean; an entry of dependencies may be a list of names.
  • Five e2e error cases beside refToNonSchemaValue.ts (in-document pointer, pointer into another file, array document, both at a property and at the root) fail on master 5caacfc and pass here; test/refTargets.test.ts pins the three shapes that keep compiling and the continueOnError lookup. New fixture test/resources/NotASchema/array.json.

Tests

  • CI=true bun run test: 688 pass, 0 fail, 351 snapshots (master 5caacfc: 680 / 0 / 351). No .snap change.
  • Corpus (node test/corpus/run.js --e2e --out …) on master and this head: 78 outputs byte-identical, 0 diagnostics. bun run conformance: all 2404 groups match the baseline.
  • The 125-row probe of $refs onto non-schema values in every schema position (checks/ref-target-probe/run.js as it was before this change), master vs this head: identical on 121 rows, the four that change are the array rows, each OK → the ReferenceError above: pos.wholeItems.arrDoc (items: {$ref: 'arr.json'} with [1,2]), root.arr ({$ref: 'arr.json'}), ptr.inDoc.required (properties/y → #/required), ok.arrDoc.props (properties/y → arr.json). pos.dependencies.listDoc does not change and must not (dependencies: {a: ['b', 'c']} is a valid schema). The 79-row root-pointer probe: 2 rows change, root.array.enum and root.array.required (unknown → the error). The 25-row two-route probe: no row changes, and no document changes route.
  • 37 array rows added to that probe (pointers onto lists in every schema position, standing in for lists of schemas, dependencies, data positions, the root with $id, chains and siblings): 24 change, all OK → the error; 12 keep compiling as they must, and the 13th (a $ref in place of a whole allOf list) crashes on both trees, per Known limits below. Four rows added to the root-pointer probe: compileFromFile, a compileFiles member and the CLI on an array root now exit 1 with the error; a root pointer into a file whose own root $ref leads onto a list is still the ref-parser's missing-pointer error on both trees, the twin of Report a root $ref onto a keyword value as not a JSON Schema, not as a missing pointer #883's cross-file case.
  • Driven through the built CLI on a file and on stdin: exit 1 and the two-line message; the six controls above compile as on master.

Known limits

  • A $ref onto a list with a type-shaping sibling ({"type": "string", "$ref": "#/definitions/unit/enum"}) is merged by the ref-parser into an object before any hook sees the list, on both dereferencing routes alike, and compiles to string (master: a tuple). Judging it would mean reading the target before the merge on the ref-parser's route, which has no hook for that; left as is, and noted in the resolver.
  • A $ref standing in for a whole allOf/anyOf/oneOf list crashes with TypeError: arr.forEach is not a function in the prenormalizer's walk, before dereferencing, on master and here alike; not this change's.
  • The position named for a $ref with an annotation sibling is the allOf/0 the prenormalizer rewrites it to, as for every non-schema target since Report a $ref whose target is not a schema (empty file, null/string/number document, plain-text YAML) instead of crashing #877.

Independent of #883 (root pointer onto a string): git merge-tree of the two heads is clean, and once both are in, an in-document root pointer onto a list is caught by #883's guard first, with the same message.

Changelog

  • A $ref in place of a schema that leads to a list (an enum, a required list, an array document) is now an error naming the list, instead of a tuple of its members or unknown.

…of emitting a tuple

A `$ref` where a schema is expected -- a property, an `items`, an `allOf` member, the
root -- that led to an array (a pointer onto an `enum` or `required` list, in this
document or another, or a file whose whole content is an array) came out as a tuple of
the list's members (`y?: ["metres", "feet"]`), or as `unknown` at the root, with no
error: the non-schema guard took any object for a schema, arrays included. It now gets
the guard's error, naming the array ("an array of 2 items") and the file, if any.

Still fine: a `$ref` onto a list in a data position (`enum`, `examples`, `default`,
vendor keys), a list of property names under `dependencies`, and a `$ref` standing in
for the list of schemas an `items` or `extends` may hold.

The root's `$ref` is looked up before dereferencing, since the ref-parser reports it to
no hook and, with a keyword beside it, merges an array's members into the root as keys
`0`, `1`, ...; the in-document path now reports where each replacement sits, as the
ref-parser does, and judges what it collected.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant