From 92e485ab82d8a97f241c7ab1ab800cc74fe68847 Mon Sep 17 00:00:00 2001 From: cliffhall Date: Wed, 26 Aug 2026 07:41:22 -0400 Subject: [PATCH] =?UTF-8?q?fix:=20address=20Copilot=20review=20round=2021?= =?UTF-8?q?=20=E2=80=94=20seed=20a=20nested=20object's=20own=20union=20bra?= =?UTF-8?q?nch?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit collectSchemaDefaults recursed only when a nested object declared `properties` directly, so one keeping its fields on a composition branch was skipped — while SchemaForm rendered that branch and displayed its read-only discriminator. The constant was shown and never submitted, and the server rejected the call. The recursion now gates on declaresAnyFields. Signed-off-by: cliffhall --- clients/web/src/utils/jsonUtils.test.ts | 31 +++++++++++++++++++++++++ clients/web/src/utils/jsonUtils.ts | 10 +++++++- 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/clients/web/src/utils/jsonUtils.test.ts b/clients/web/src/utils/jsonUtils.test.ts index 0178ff12..228f81f3 100644 --- a/clients/web/src/utils/jsonUtils.test.ts +++ b/clients/web/src/utils/jsonUtils.test.ts @@ -457,6 +457,37 @@ describe("root composition (#2123)", () => { expect(Object.hasOwn(seeded, "__proto__")).toBe(true); }); + it("seeds defaults from a nested object's own union branch", () => { + // The nested form renders that branch, so its read-only discriminator has + // to reach the submitted values too. + expect( + collectSchemaDefaults({ + type: "object", + properties: { + config: { + type: "object", + anyOf: [ + { + type: "object", + properties: { + kind: { type: "string", const: "email" }, + address: { type: "string" }, + }, + }, + { + type: "object", + properties: { + kind: { type: "string", const: "sms" }, + phone: { type: "string" }, + }, + }, + ], + }, + }, + }), + ).toEqual({ config: { kind: "email" } }); + }); + it("re-applies a nested object's constants", () => { // The overlay replaces the whole nested object rather than merging into it, // so a link naming `{ config: { kind: "sms" } }` would otherwise slip past diff --git a/clients/web/src/utils/jsonUtils.ts b/clients/web/src/utils/jsonUtils.ts index 4f1838c8..acf5f0a7 100644 --- a/clients/web/src/utils/jsonUtils.ts +++ b/clients/web/src/utils/jsonUtils.ts @@ -4,6 +4,7 @@ import { } from "@inspector/core/json/nullableUnion.js"; import { branchAcceptsValues, + declaresAnyFields, resolveRootUnion, selectBranchIndex, } from "@inspector/core/json/rootUnion.js"; @@ -176,7 +177,14 @@ export function collectSchemaDefaults( seed(fieldName, fieldSchema.const); } else if (fieldSchema.default !== undefined) { seed(fieldName, fieldSchema.default); - } else if (fieldSchema.type === "object" && fieldSchema.properties) { + } else if ( + fieldSchema.type === "object" && + // Not `fieldSchema.properties`: a nested object can keep its fields on a + // composition branch too, and `SchemaForm` renders that branch — so + // skipping it here would leave its read-only discriminator displayed but + // never submitted, and the server would reject the call (#2123). + declaresAnyFields(fieldSchema) + ) { const nested = collectSchemaDefaults(fieldSchema); if (Object.keys(nested).length > 0) { seed(fieldName, nested);