diff --git a/clients/web/src/test/core/rootUnion.test.ts b/clients/web/src/test/core/rootUnion.test.ts index bc29fbfe..1fddfb0c 100644 --- a/clients/web/src/test/core/rootUnion.test.ts +++ b/clients/web/src/test/core/rootUnion.test.ts @@ -764,9 +764,9 @@ describe("resolveRootUnion", () => { } }); - it("offers a member carrying a boolean property schema", () => { - // JSON Schema's boolean form is legal and answers every keyword lookup - // with `undefined`, so it renders through the JSON fallback harmlessly. + it("offers a member carrying a `true` property schema", () => { + // `true` constrains nothing and answers every keyword lookup with + // `undefined`, so it renders through the JSON fallback harmlessly. const { branches } = resolveRootUnion({ type: "object", anyOf: [ @@ -777,6 +777,19 @@ describe("resolveRootUnion", () => { expect(branches).toHaveLength(2); }); + it("declines a member carrying a `false` property schema", () => { + // `false` admits no value at all, so the field can never be filled — and + // a required one makes the branch unsatisfiable. + const { branches } = resolveRootUnion({ + type: "object", + anyOf: [ + { type: "object", properties: { nothing: false } }, + { type: "object", properties: { other: { type: "string" } } }, + ] as unknown[], + }); + expect(branches).toEqual([]); + }); + it("declines a member whose properties are not an object", () => { // Members arrive as `unknown`, so this is reachable and must not throw. expect( diff --git a/core/json/rootUnion.ts b/core/json/rootUnion.ts index 25059afe..06104742 100644 --- a/core/json/rootUnion.ts +++ b/core/json/rootUnion.ts @@ -180,15 +180,17 @@ function isOfferable(branch: RootUnionSchema): boolean { const properties = Object.values(propertiesOf(branch)); return ( properties.length > 0 && - // Every value has to be something a renderer can read. JSON Schema's - // boolean form is legal and harmless — `true`/`false` answer every keyword - // lookup with `undefined` — but a `null` or an array is not a schema at - // all, and the web form dereferences one on the way to choosing a widget. - // A branch carrying one is declined rather than handed on to crash a tool - // panel that would otherwise have rendered. + // Every value has to be something a renderer can read AND something a + // caller can satisfy. A `null` or an array is not a schema at all, and the + // web form dereferences one on the way to choosing a widget. JSON Schema's + // boolean form is legal, but only `true` is harmless — it constrains + // nothing and answers every keyword lookup with `undefined`, while `false` + // admits no value whatsoever, so a field declared with it can never be + // filled and a required one makes the whole branch unsatisfiable. Either + // way the branch is declined rather than offered as a callable shape. properties.every( (property) => - typeof property === "boolean" || + property === true || (typeof property === "object" && property !== null && !Array.isArray(property)),