@@ -10,7 +10,7 @@ import { Button } from "@/components/ui/button";
1010import { Input } from "@/components/ui/input" ;
1111import JsonEditor from "./JsonEditor" ;
1212import { updateValueAtPath } from "@/utils/jsonUtils" ;
13- import { generateDefaultValue } from "@/utils/schemaUtils" ;
13+ import { generateDefaultValue , normalizeUnionType } from "@/utils/schemaUtils" ;
1414import type {
1515 JsonValue ,
1616 JsonSchemaType ,
@@ -87,6 +87,9 @@ const DynamicJsonForm = forwardRef<DynamicJsonFormRef, DynamicJsonFormProps>(
8787 // - Arrays with defined items are form-capable
8888 // - Primitive types are form-capable
8989 const canRenderTopLevelForm = ( s : JsonSchemaType ) : boolean => {
90+ // Unwrap Optional[X] at the top level so anyOf:[X,null] is treated as X
91+ s = normalizeUnionType ( s ) ;
92+
9093 const primitiveTypes = [ "string" , "number" , "integer" , "boolean" , "null" ] ;
9194
9295 const hasType = Array . isArray ( s . type ) ? s . type . length > 0 : ! ! s . type ;
@@ -303,6 +306,10 @@ const DynamicJsonForm = forwardRef<DynamicJsonFormRef, DynamicJsonFormProps>(
303306 parentSchema ?: JsonSchemaType ,
304307 propertyName ?: string ,
305308 ) => {
309+ // Unwrap Optional[X] / nullable unions (anyOf: [X, null]) before ANY type checks
310+ // so that maxDepth enforcement and the type switch both see the real type.
311+ propSchema = normalizeUnionType ( propSchema ) ;
312+
306313 if (
307314 depth >= maxDepth &&
308315 ( propSchema . type === "object" || propSchema . type === "array" )
@@ -601,7 +608,10 @@ const DynamicJsonForm = forwardRef<DynamicJsonFormRef, DynamicJsonFormProps>(
601608 if ( ! propSchema . items ) return null ;
602609
603610 // Special handling: array of enums -> render multi-select control
604- const itemSchema = propSchema . items as JsonSchemaType ;
611+ // Normalize items so Optional[X] (anyOf:[X,null]) is unwrapped correctly.
612+ const itemSchema = normalizeUnionType (
613+ propSchema . items as JsonSchemaType ,
614+ ) ;
605615 let multiOptions : { value : string ; label : string } [ ] | null = null ;
606616
607617 const titledMulti = (
@@ -666,8 +676,11 @@ const DynamicJsonForm = forwardRef<DynamicJsonFormRef, DynamicJsonFormProps>(
666676 ) ;
667677 }
668678
669- // If the array items are simple, render as form fields, otherwise use JSON editor
670- if ( isSimpleObject ( propSchema . items ) ) {
679+ // Typed object items → structured form with Add/Remove; untyped → JSON fallback
680+ const itemIsObject =
681+ itemSchema . type === "object" && ! ! itemSchema . properties ;
682+
683+ if ( isSimpleObject ( itemSchema ) || itemIsObject ) {
671684 return (
672685 < div className = "space-y-4" >
673686 { propSchema . description && (
@@ -676,46 +689,68 @@ const DynamicJsonForm = forwardRef<DynamicJsonFormRef, DynamicJsonFormProps>(
676689 </ p >
677690 ) }
678691
679- { propSchema . items ? .description && (
692+ { itemSchema . description && (
680693 < p className = "text-sm text-gray-500" >
681- Items: { propSchema . items . description }
694+ { itemSchema . description }
682695 </ p >
683696 ) }
684697
685698 < div className = "space-y-2" >
686- { arrayValue . map ( ( item , index ) => (
687- < div key = { index } className = "flex items-center gap-2" >
688- { renderFormFields (
689- propSchema . items as JsonSchemaType ,
690- item ,
691- [ ...path , index . toString ( ) ] ,
692- depth + 1 ,
693- ) }
694- < Button
695- variant = "outline"
696- size = "sm"
697- onClick = { ( ) => {
698- const newArray = [ ...arrayValue ] ;
699- newArray . splice ( index , 1 ) ;
700- handleFieldChange ( path , newArray ) ;
701- } }
702- >
703- Remove
704- </ Button >
705- </ div >
706- ) ) }
699+ { arrayValue . map ( ( item , index ) =>
700+ itemIsObject ? (
701+ < div key = { index } className = "space-y-2" >
702+ { renderFormFields (
703+ itemSchema ,
704+ item ,
705+ [ ...path , index . toString ( ) ] ,
706+ depth + 1 ,
707+ ) }
708+ < div className = "flex justify-end" >
709+ < Button
710+ variant = "outline"
711+ size = "sm"
712+ onClick = { ( ) => {
713+ const newArray = [ ...arrayValue ] ;
714+ newArray . splice ( index , 1 ) ;
715+ handleFieldChange ( path , newArray ) ;
716+ } }
717+ >
718+ Remove
719+ </ Button >
720+ </ div >
721+ </ div >
722+ ) : (
723+ < div key = { index } className = "flex items-center gap-2" >
724+ { renderFormFields (
725+ itemSchema ,
726+ item ,
727+ [ ...path , index . toString ( ) ] ,
728+ depth + 1 ,
729+ ) }
730+ < Button
731+ variant = "outline"
732+ size = "sm"
733+ onClick = { ( ) => {
734+ const newArray = [ ...arrayValue ] ;
735+ newArray . splice ( index , 1 ) ;
736+ handleFieldChange ( path , newArray ) ;
737+ } }
738+ >
739+ Remove
740+ </ Button >
741+ </ div >
742+ ) ,
743+ ) }
707744 < Button
708745 variant = "outline"
709746 size = "sm"
710747 onClick = { ( ) => {
711- const defaultValue = getArrayItemDefault (
712- propSchema . items as JsonSchemaType ,
713- ) ;
748+ const defaultValue = getArrayItemDefault ( itemSchema ) ;
714749 handleFieldChange ( path , [ ...arrayValue , defaultValue ] ) ;
715750 } }
716751 title = {
717- propSchema . items ? .description
718- ? `Add new ${ propSchema . items . description } `
752+ itemSchema . description
753+ ? `Add new ${ itemSchema . description } `
719754 : "Add new item"
720755 }
721756 >
@@ -726,7 +761,7 @@ const DynamicJsonForm = forwardRef<DynamicJsonFormRef, DynamicJsonFormProps>(
726761 ) ;
727762 }
728763
729- // For complex arrays, fall back to JSON editor
764+ // For truly unstructured arrays (no type or no properties) , fall back to JSON editor
730765 return (
731766 < JsonEditor
732767 value = { JSON . stringify ( currentValue ?? [ ] , null , 2 ) }
0 commit comments