diff --git a/client/src/components/DynamicJsonForm.tsx b/client/src/components/DynamicJsonForm.tsx
index 90249ab1c..84dd41872 100644
--- a/client/src/components/DynamicJsonForm.tsx
+++ b/client/src/components/DynamicJsonForm.tsx
@@ -2,6 +2,7 @@ import {
useState,
useEffect,
useCallback,
+ useMemo,
useRef,
forwardRef,
useImperativeHandle,
@@ -10,7 +11,11 @@ import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import JsonEditor from "./JsonEditor";
import { updateValueAtPath } from "@/utils/jsonUtils";
-import { generateDefaultValue } from "@/utils/schemaUtils";
+import {
+ generateDefaultValue,
+ mergeAllOf,
+ resolveRef,
+} from "@/utils/schemaUtils";
import type {
JsonValue,
JsonSchemaType,
@@ -55,6 +60,44 @@ const isSimpleObject = (schema: JsonSchemaType): boolean => {
return false;
};
+// A oneOf whose members are full schemas is a variant union rendered with a
+// selector. oneOf members carrying const are titled enum options and keep
+// their existing select rendering, as do schemas that already render on
+// their own (a type other than a property-less object).
+const getVariantOptions = (schema: JsonSchemaType): JsonSchemaType[] | null => {
+ if (!schema.oneOf || schema.oneOf.length === 0) return null;
+ if (schema.oneOf.some((opt) => "const" in opt)) return null;
+ if (schema.type && !(schema.type === "object" && !schema.properties)) {
+ return null;
+ }
+ return schema.oneOf as JsonSchemaType[];
+};
+
+// Picks the variant whose properties best match the keys already present in
+// the value, so an existing value does not silently render the first variant
+const inferVariantIndex = (
+ variants: JsonSchemaType[],
+ value: JsonValue,
+): number | undefined => {
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
+ return undefined;
+ }
+ const keys = Object.keys(value);
+ if (keys.length === 0) return undefined;
+
+ let bestIdx: number | undefined;
+ let bestScore = 0;
+ variants.forEach((variant, idx) => {
+ const props = variant.properties ?? {};
+ const score = keys.filter((key) => key in props).length;
+ if (score > bestScore) {
+ bestScore = score;
+ bestIdx = idx;
+ }
+ });
+ return bestIdx;
+};
+
const getArrayItemDefault = (schema: JsonSchemaType): JsonValue => {
if ("default" in schema && schema.default !== undefined) {
return schema.default;
@@ -81,14 +124,20 @@ const getArrayItemDefault = (schema: JsonSchemaType): JsonValue => {
const DynamicJsonForm = forwardRef {propSchema.description}