diff --git a/packages/core/src/execution-engine/node-execution-context/utils/__tests__/create-node-as-tool.test.ts b/packages/core/src/execution-engine/node-execution-context/utils/__tests__/create-node-as-tool.test.ts index 450ec99247..98827bce61 100644 --- a/packages/core/src/execution-engine/node-execution-context/utils/__tests__/create-node-as-tool.test.ts +++ b/packages/core/src/execution-engine/node-execution-context/utils/__tests__/create-node-as-tool.test.ts @@ -319,7 +319,7 @@ describe('createNodeAsTool', () => { const tool = createNodeAsTool(options).response; - expect(tool.schema.shape.complexJson._def.innerType).toBeInstanceOf(z.ZodRecord); + expect(tool.schema.shape.complexJson._def.innerType).toBeInstanceOf(z.ZodEffects); expect(tool.schema.shape.complexJson.description).toBe('Param with complex JSON default'); expect(tool.schema.shape.complexJson._def.defaultValue()).toEqual({ nested: { key: 'value' }, diff --git a/packages/workflow/src/FromAIParseUtils.ts b/packages/workflow/src/FromAIParseUtils.ts index 4d86040b23..b96c91ec25 100644 --- a/packages/workflow/src/FromAIParseUtils.ts +++ b/packages/workflow/src/FromAIParseUtils.ts @@ -34,9 +34,61 @@ export function generateZodSchema(placeholder: FromAIArgument): z.ZodTypeAny { case 'boolean': schema = z.boolean(); break; - case 'json': - schema = z.record(z.any()); + case 'json': { + interface CustomSchemaDef extends z.ZodTypeDef { + jsonSchema?: { + anyOf: [ + { + type: 'object'; + minProperties: number; + additionalProperties: boolean; + }, + { + type: 'array'; + minItems: number; + }, + ]; + }; + } + + // Create a custom schema to validate that the incoming data is either a non-empty object or a non-empty array. + const customSchema = z.custom | unknown[]>( + (data: unknown) => { + if (data === null || typeof data !== 'object') return false; + if (Array.isArray(data)) { + return data.length > 0; + } + return Object.keys(data).length > 0; + }, + { + message: 'Value must be a non-empty object or a non-empty array', + }, + ); + + // Cast the custom schema to a type that includes our JSON metadata. + const typedSchema = customSchema as z.ZodType< + Record | unknown[], + CustomSchemaDef + >; + + // Attach the updated `jsonSchema` metadata to the internal definition. + typedSchema._def.jsonSchema = { + anyOf: [ + { + type: 'object', + minProperties: 1, + additionalProperties: true, + }, + { + type: 'array', + minItems: 1, + }, + ], + }; + + schema = typedSchema; break; + } default: schema = z.string(); } diff --git a/packages/workflow/test/FromAIParseUtils.test.ts b/packages/workflow/test/FromAIParseUtils.test.ts index bfe1a52fb0..7f55ebf746 100644 --- a/packages/workflow/test/FromAIParseUtils.test.ts +++ b/packages/workflow/test/FromAIParseUtils.test.ts @@ -1,7 +1,8 @@ import { extractFromAICalls, - type FromAIArgument, traverseNodeParameters, + type FromAIArgument, + generateZodSchema, } from '@/FromAIParseUtils'; // Note that for historic reasons a lot of testing of this file happens indirectly in `packages/core/test/CreateNodeAsTool.test.ts` @@ -85,3 +86,61 @@ describe('traverseNodeParameters', () => { }, ); }); + +describe('JSON Type Parsing via generateZodSchema', () => { + it('should correctly parse a JSON parameter without default', () => { + // Use an actual $fromAI call string via extractFromAICalls: + const [arg] = extractFromAICalls( + '$fromAI("jsonWithoutDefault", "JSON parameter without default", "json")', + ); + const schema = generateZodSchema(arg); + + // Valid non-empty JSON objects should pass. + expect(() => schema.parse({ key: 'value' })).not.toThrow(); + expect(schema.parse({ key: 'value' })).toEqual({ key: 'value' }); + + // Parsing an empty object should throw a validation error. + expect(() => schema.parse({})).toThrowError( + /Value must be a non-empty object or a non-empty array/, + ); + }); + + it('should correctly parse a JSON parameter with a valid default', () => { + const [arg] = extractFromAICalls( + '$fromAI("jsonWithValidDefault", "JSON parameter with valid default", "json", "{"key": "defaultValue"}")', + ); + const schema = generateZodSchema(arg); + + // The default value is now stored as a parsed object. + expect(schema._def.defaultValue()).toEqual({ key: 'defaultValue' }); + }); + + it('should parse a JSON parameter with an empty default', () => { + const [arg] = extractFromAICalls( + '$fromAI("jsonEmptyDefault", "JSON parameter with empty default", "json", "{}")', + ); + const schema = generateZodSchema(arg); + + // The default value is stored as an empty object. + expect(schema._def.defaultValue()).toEqual({}); + + // Parsing an empty object should throw a validation error. + expect(() => schema.parse({})).toThrowError( + /Value must be a non-empty object or a non-empty array/, + ); + }); + + it('should use provided JSON value over the default value', () => { + const [arg] = extractFromAICalls( + '$fromAI("jsonParamCustom", "JSON parameter with custom default", "json", "{"initial": "value"}")', + ); + const schema = generateZodSchema(arg); + + // Check that the stored default value parses to the expected object. + expect(schema._def.defaultValue()).toEqual({ initial: 'value' }); + + // When a new valid value is provided, the schema should use it. + const newValue = { newKey: 'newValue' }; + expect(schema.parse(newValue)).toEqual(newValue); + }); +});