fix: address Copilot review round 21 — seed a nested object's own union branch

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 <cliff@futurescale.com>
This commit is contained in:
cliffhall 2026-08-26 07:41:22 -04:00
commit 92e485ab82
2 changed files with 40 additions and 1 deletions

View file

@ -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

View file

@ -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);