From d8ce73c1df0c2f291901f6996ad2e69b4dd772ae Mon Sep 17 00:00:00 2001 From: Charlie Kolb Date: Fri, 28 Aug 2026 11:43:19 +0000 Subject: [PATCH] fix(Form Node): Improve error messages for invalid form definition JSON (#37291) Co-authored-by: Claude Fable 5 --- packages/workflow/src/type-validation.ts | 114 ++++++++++-------- .../workflow/test/type-validation.test.ts | 35 ++++++ 2 files changed, 99 insertions(+), 50 deletions(-) diff --git a/packages/workflow/src/type-validation.ts b/packages/workflow/src/type-validation.ts index 84970d98fde..f709e2c6111 100644 --- a/packages/workflow/src/type-validation.ts +++ b/packages/workflow/src/type-validation.ts @@ -203,69 +203,83 @@ const ALLOWED_FIELD_TYPES = [ 'hiddenField', ]; +const tryToParseFormFieldOptions = (rawOptions: unknown, index: number): object => { + const asObject = Array.isArray(rawOptions) ? { values: rawOptions } : rawOptions; + + const invalidValuesError = new UserError( + `Field dropdown in field ${index} has no 'values' property that contains an array of options`, + ); + + if (typeof asObject !== 'object' || asObject === null) throw invalidValuesError; + + const { values } = asObject as { [key: string]: unknown }; + if (!Array.isArray(values)) throw invalidValuesError; + + const options: unknown[] = values; + for (const [optionIndex, option] of options.entries()) { + if ( + typeof option !== 'object' || + option === null || + Object.keys(option).length !== 1 || + typeof (option as { option?: unknown }).option !== 'string' + ) { + throw new UserError(`Field dropdown in field ${index} has an invalid option ${optionIndex}`); + } + } + + return asObject; +}; + export const tryToParseJsonToFormFields = (value: unknown): FormFieldsParameter => { const fields: FormFieldsParameter = []; + let rawFields: Array<{ [key: string]: unknown }>; try { - const rawFields = jsonParse>(value as string, { + rawFields = jsonParse>(value as string, { acceptJSObject: true, }); + } catch (error) { + // Keep the engine message, it contains the error position + throw new UserError(`Value is not valid JSON: ${(error as Error).message}`); + } - for (const [index, field] of rawFields.entries()) { - for (const key of Object.keys(field)) { - if (!ALLOWED_FORM_FIELDS_KEYS.includes(key)) { - throw new UserError(`Key '${key}' in field ${index} is not valid for form fields`); - } - if ( - key !== 'fieldOptions' && - !['string', 'number', 'boolean'].includes(typeof field[key]) - ) { - field[key] = String(field[key]); - } else if (typeof field[key] === 'string' && key !== 'html') { - field[key] = field[key].replace(//g, '>'); - } + if (!Array.isArray(rawFields)) { + throw new UserError( + `Form fields must be an array of objects, but we got ${getValueDescription(rawFields)}`, + ); + } - if (key === 'fieldType' && !ALLOWED_FIELD_TYPES.includes(field[key] as string)) { - throw new UserError( - `Field type '${field[key] as string}' in field ${index} is not valid for form fields`, - ); - } + for (const [index, field] of rawFields.entries()) { + if (typeof field !== 'object' || field === null || Array.isArray(field)) { + throw new UserError( + `Field ${index} must be an object, but we got ${getValueDescription(field)}`, + ); + } - if (key === 'fieldOptions') { - if (Array.isArray(field[key])) { - field[key] = { values: field[key] }; - } - - if ( - typeof field[key] !== 'object' || - !(field[key] as { [key: string]: unknown }).values - ) { - throw new UserError( - `Field dropdown in field ${index} does has no 'values' property that contain an array of options`, - ); - } - - for (const [optionIndex, option] of ( - (field[key] as { [key: string]: unknown }).values as Array<{ - [key: string]: { option: string }; - }> - ).entries()) { - if (Object.keys(option).length !== 1 || typeof option.option !== 'string') { - throw new UserError( - `Field dropdown in field ${index} has an invalid option ${optionIndex}`, - ); - } - } - } + for (const key of Object.keys(field)) { + if (!ALLOWED_FORM_FIELDS_KEYS.includes(key)) { + throw new UserError(`Key '${key}' in field ${index} is not valid for form fields`); + } + if (key !== 'fieldOptions' && !['string', 'number', 'boolean'].includes(typeof field[key])) { + field[key] = String(field[key]); + } else if (typeof field[key] === 'string' && key !== 'html') { + field[key] = field[key].replace(//g, '>'); } - fields.push(field as FormFieldsParameter[number]); - } - } catch (error) { - if (error instanceof ApplicationError || error instanceof BaseError) throw error; + if (key === 'fieldType' && !ALLOWED_FIELD_TYPES.includes(field[key] as string)) { + throw new UserError( + `Field type '${field[key] as string}' in field ${index} is not valid for form fields`, + ); + } - throw new UserError('Value is not valid JSON'); + if (key === 'fieldOptions') { + field[key] = tryToParseFormFieldOptions(field[key], index); + } + } + + fields.push(field as FormFieldsParameter[number]); } + return fields; }; diff --git a/packages/workflow/test/type-validation.test.ts b/packages/workflow/test/type-validation.test.ts index 08066f70a79..115f5a7b77c 100644 --- a/packages/workflow/test/type-validation.test.ts +++ b/packages/workflow/test/type-validation.test.ts @@ -372,6 +372,41 @@ describe('Type Validation', () => { const fields = tryToParseJsonToFormFields(json); expect(fields).toEqual([{ fieldType: 'html', html: '
test
' }]); }); + + it('should include the parse error detail for invalid JSON', () => { + const json = '[{"fieldLabel": }]'; + expect(() => tryToParseJsonToFormFields(json)).toThrowError(/^Value is not valid JSON: .+/); + }); + + it('should reject valid JSON that is not an array', () => { + const json = '{"fieldLabel": "Name"}'; + expect(() => tryToParseJsonToFormFields(json)).toThrowError( + 'Form fields must be an array of objects, but we got object', + ); + }); + + it('should reject array entries that are not objects', () => { + const json = '[null]'; + expect(() => tryToParseJsonToFormFields(json)).toThrowError( + "Field 0 must be an object, but we got 'null'", + ); + }); + + it('should reject fieldOptions without a values array', () => { + const json = + '[{"fieldLabel": "Choice", "fieldType": "dropdown", "fieldOptions": {"values": "not an array"}}]'; + expect(() => tryToParseJsonToFormFields(json)).toThrowError( + "Field dropdown in field 0 has no 'values' property that contains an array of options", + ); + }); + + it('should reject dropdown options that are not objects', () => { + const json = + '[{"fieldLabel": "Choice", "fieldType": "dropdown", "fieldOptions": {"values": [null]}}]'; + expect(() => tryToParseJsonToFormFields(json)).toThrowError( + 'Field dropdown in field 0 has an invalid option 0', + ); + }); }); describe('binary', () => {