Skip to content

Accept a $ref in place of a whole allOf/anyOf/oneOf/prefixItems list instead of a TypeError - #888

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

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

Conversation

@claude

@claude claude Bot commented Sep 7, 2026 •

Copy link
Copy Markdown
Contributor

Output change: none for any existing schema (no snapshot entry moves; three are added for the new cases). Invalid schemas that master compiled around are now rejected: see "Also rejected" below. Draft until its check passes.

A $ref standing in for a whole allOf, anyOf, oneOf or prefixItems list -- allOf: {$ref: "mixins.json"}, or {$ref: "#/definitions/base/allOf"} -- crashed before anything was dereferenced with TypeError: arr.forEach is not a function (the walk in src/utils.ts assumed an array). Now it compiles when the $ref leads to a list of schemas, and is rejected with the keyword, its position and the $ref when it leads to anything else.

Before / after, for record: {type: "object", allOf: {$ref: "SchemaLists/ObjectSchemas.json"}} (a file holding two object schemas) and value: {anyOf: {$ref: "SchemaLists/lists.json#/scalars"}}:

// before: TypeError: arr.forEach is not a function
// after:
export interface RefInPlaceOfSchemaList {
  record: { id: string } & { createdAt?: number };
  value: string | number;
}

When the $ref leads to something that is no list of schemas:

anyOf at properties/status must be a list of schemas: $ref "#/definitions/status/enum" leads to a list whose item 0 is the string "on".
allOf at properties/person must be a list of schemas: $ref "Person.json" leads to an object (one schema, not a list of them).
allOf at properties/record must be a list of schemas: $ref "#/definitions/base/allOf" leads to one, but the keywords beside it (description) make an object of it (a $ref standing in for a whole list stands alone).

A $ref in a list position leading to a string, null or an empty file was already reported (#877); that report is unchanged and still comes first. A title beside the stand-in is dropped (as beside any $ref); any other sibling keyword is rejected as above.

Also rejected, where master compiled around an invalid schema: a null, false, 0 or "" written in place of a list (allOf: null compiled to the object type; prefixItems: null to unknown[]) now gets the same "must be a list of schemas: found …" message; and a bad stand-in under OpenAPI components/schemas, which master never walked and printed as a literal type (export type Pet = "x" & "y"), gets the message too. Incidentally fixed: a property named allOf under a keyword this tool does not know (contains, propertyNames) crashed master with the same TypeError and now compiles.

How

  • traverse visits a non-array value under a list keyword as a schema, so the {$ref} reaches the ref-parser like any other.
  • The resolver judges what it led to once dereferencing is done (rejectNonSchemaLists): a walk over the schema positions from the root, throwing for a list keyword whose value is not an array, or is an array a $ref led to whose members are not all schemas. A list written in place keeps today's member-by-member judgement, so this does not overlap 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.
  • That walk costs about what the validator's does (2 % of compile() on the Azure template), so it runs only for documents the prenormalizer marked as holding a $ref or a non-schema at a list keyword (ListsToJudge), and for a file that is itself a list of schemas. Real schemas have properties named allOf (Azure's Microsoft.Security does), so a schema object without a $ref is not a mark. Master vs this head, 7 alternating runs, min/median ms: Azure 5209/5263 vs 5154/5256; FHIR 537/551 vs 525/552.

Tests

Seven e2e cases, test/e2e/refInPlaceOfSchemaList.*.ts: external file (a whole-file array and pointers into a file), in-document pointers, a stand-in inside a file that is itself a list, and four rejections (an enum's values, one schema, a sibling keyword, null in place). Six fail on master with the TypeError; inArrayDocument already compiled there and guards the gate.

  • CI=true bun run test: 690 pass, 0 fail (master 5caacfc: 686).
  • bun run corpus and bun run conformance: same as master (26 × 3 compile and type-check; all 2404 groups match the baseline).
  • The 154-row $ref-target probe differs from master in exactly the whole-list rows pos.wholeAllOf.str (TypeError → the Report a $ref whose target is not a schema (empty file, null/string/number document, plain-text YAML) instead of crashing #877 report) and arr.wholeAllOf.schemasDoc (TypeError → string & number); the array probe in its four allOf.whole.*/anyOf.whole.* rows only.
  • CLI: json2ts on a schema with the stand-in prints the type; on a bad one prints the message above.

Not changed: a list written in place whose members are no schemas (allOf: [1, 2], a member $ref onto an enum; #886 covers the latter), and a schema object written in place of a list (allOf: {type: "number"}), which still reaches the parser's members.map TypeError as on master.

git merge-tree against every open draft, #879 to #887: all clean.

Changelog

  • Fixed: a $ref standing in for a whole allOf/anyOf/oneOf/prefixItems list compiles, or is rejected by position, instead of throwing TypeError: arr.forEach is not a function.

…ems` list, and reject one that leads to no list of schemas by position, instead of a TypeError

Before dereferencing, `traverse` assumed a list keyword's value is an array and died with
`TypeError: arr.forEach is not a function` on the `{$ref}` object standing in for it. The walk
now visits such a value as a schema, and the resolver judges what it led to once dereferencing
is done: an array of schemas compiles (an intersection, a union, a tuple); anything else --
a file holding one schema, an `enum`'s values, `true`, `null` -- is reported with the keyword,
its position and the `$ref`. The judging walk runs only for documents the prenormalizer marked
as holding such a value, so schemas without one pay nothing for it.
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