From 6ac45d9b26956dfcbae7c74905ef0de6bee4914b Mon Sep 17 00:00:00 2001 From: cliffhall Date: Wed, 26 Aug 2026 10:23:35 -0400 Subject: [PATCH] =?UTF-8?q?fix:=20address=20Copilot=20review=20round=2028?= =?UTF-8?q?=20=E2=80=94=20the=20boolean=20schema=20form=20in=20a=20null-ad?= =?UTF-8?q?mitting=20union?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `anyOfAdmitsNull` sent every non-object member to `toBranch` and refused it, so `{ const: null, anyOf: [true] }` read as non-nullable — a required field seeded and rendered read-only as `null` and reported missing forever. `true` is the unconstrained schema and admits every value, `null` among them; `false` admits none and still does not. Signed-off-by: cliffhall --- clients/web/src/test/core/nullableUnion.test.ts | 6 ++++++ core/json/nullableUnion.ts | 3 +++ 2 files changed, 9 insertions(+) diff --git a/clients/web/src/test/core/nullableUnion.test.ts b/clients/web/src/test/core/nullableUnion.test.ts index c1054c83..24592e24 100644 --- a/clients/web/src/test/core/nullableUnion.test.ts +++ b/clients/web/src/test/core/nullableUnion.test.ts @@ -768,6 +768,12 @@ describe("admitsNull", () => { ); }); + it("reads the boolean schema form", () => { + // `true` is the unconstrained schema; `false` admits nothing at all. + expect(admitsNull({ const: null, anyOf: [true] })).toBe(true); + expect(admitsNull({ const: null, anyOf: [false] })).toBe(false); + }); + it("does not override a sibling that rejects null", () => { // `const` is conjunctive with its siblings, not an override: both of // these reject every value, so claiming nullability would let the diff --git a/core/json/nullableUnion.ts b/core/json/nullableUnion.ts index 3217b0f1..fd6761dc 100644 --- a/core/json/nullableUnion.ts +++ b/core/json/nullableUnion.ts @@ -454,6 +454,9 @@ function anyOfAdmitsNull(schema: NullableUnionSchema): boolean { const branches = schema.anyOf; if (!Array.isArray(branches)) return false; return branches.some((entry) => { + // JSON Schema's boolean form: `true` is the unconstrained schema and admits + // every value, `null` among them, while `false` admits none. + if (typeof entry === "boolean") return entry; const branch = toBranch(entry); if (branch === null) return false; const branchSchema = branch as NullableUnionSchema;