diff --git a/packages/@n8n/task-runner/src/js-task-runner/__tests__/js-task-runner.test.ts b/packages/@n8n/task-runner/src/js-task-runner/__tests__/js-task-runner.test.ts
index 61c19c5cc2..d2dac70778 100644
--- a/packages/@n8n/task-runner/src/js-task-runner/__tests__/js-task-runner.test.ts
+++ b/packages/@n8n/task-runner/src/js-task-runner/__tests__/js-task-runner.test.ts
@@ -1,6 +1,10 @@
import { DateTime, Duration, Interval } from 'luxon';
-import type { IBinaryData } from 'n8n-workflow';
-import { setGlobalState, type CodeExecutionMode, type IDataObject } from 'n8n-workflow';
+import {
+ type IBinaryData,
+ setGlobalState,
+ type CodeExecutionMode,
+ type IDataObject,
+} from 'n8n-workflow';
import fs from 'node:fs';
import { builtinModules } from 'node:module';
@@ -26,6 +30,7 @@ import {
withPairedItem,
wrapIntoJson,
} from './test-data';
+import { ReservedKeyFoundError } from '../errors/reserved-key-not-found.error';
jest.mock('ws');
@@ -805,6 +810,15 @@ describe('JsTaskRunner', () => {
}),
).rejects.toThrow(ValidationError);
});
+
+ it('should throw a ReservedKeyFoundError if there are unknown keys alongside reserved keys', async () => {
+ await expect(
+ executeForAllItems({
+ code: 'return [{json: {b: 1}, objectId: "123"}]',
+ inputItems: [{ a: 1 }],
+ }),
+ ).rejects.toThrow(ReservedKeyFoundError);
+ });
});
it('should return static items', async () => {
diff --git a/packages/@n8n/task-runner/src/js-task-runner/errors/reserved-key-not-found.error.ts b/packages/@n8n/task-runner/src/js-task-runner/errors/reserved-key-not-found.error.ts
new file mode 100644
index 0000000000..2e32466b85
--- /dev/null
+++ b/packages/@n8n/task-runner/src/js-task-runner/errors/reserved-key-not-found.error.ts
@@ -0,0 +1,11 @@
+import { ValidationError } from './validation-error';
+
+export class ReservedKeyFoundError extends ValidationError {
+ constructor(reservedKey: string, itemIndex: number) {
+ super({
+ message: 'Invalid output format',
+ description: `An output item contains the reserved key ${reservedKey}. To get around this, please wrap each item in an object, under a key called json. Example`,
+ itemIndex,
+ });
+ }
+}
diff --git a/packages/@n8n/task-runner/src/js-task-runner/result-validation.ts b/packages/@n8n/task-runner/src/js-task-runner/result-validation.ts
index d5999d1ce4..dd5378a916 100644
--- a/packages/@n8n/task-runner/src/js-task-runner/result-validation.ts
+++ b/packages/@n8n/task-runner/src/js-task-runner/result-validation.ts
@@ -1,6 +1,7 @@
import { normalizeItems } from 'n8n-core';
import type { INodeExecutionData } from 'n8n-workflow';
+import { ReservedKeyFoundError } from './errors/reserved-key-not-found.error';
import { ValidationError } from './errors/validation-error';
import { isObject } from './obj-utils';
@@ -19,17 +20,28 @@ export const REQUIRED_N8N_ITEM_KEYS = new Set([
]);
function validateTopLevelKeys(item: INodeExecutionData, itemIndex: number) {
- for (const key in item) {
- if (Object.prototype.hasOwnProperty.call(item, key)) {
- if (REQUIRED_N8N_ITEM_KEYS.has(key)) continue;
+ let foundReservedKey: string | null = null;
+ const unknownKeys: string[] = [];
- throw new ValidationError({
- message: `Unknown top-level item key: ${key}`,
- description: 'Access the properties of an item under `.json`, e.g. `item.json`',
- itemIndex,
- });
+ for (const key in item) {
+ if (!Object.prototype.hasOwnProperty.call(item, key)) continue;
+
+ if (REQUIRED_N8N_ITEM_KEYS.has(key)) {
+ foundReservedKey ??= key;
+ } else {
+ unknownKeys.push(key);
}
}
+
+ if (unknownKeys.length > 0) {
+ if (foundReservedKey) throw new ReservedKeyFoundError(foundReservedKey, itemIndex);
+
+ throw new ValidationError({
+ message: `Unknown top-level item key: ${unknownKeys[0]}`,
+ description: 'Access the properties of an item under `.json`, e.g. `item.json`',
+ itemIndex,
+ });
+ }
}
function validateItem({ json, binary }: INodeExecutionData, itemIndex: number) {
diff --git a/packages/nodes-base/nodes/Code/Sandbox.ts b/packages/nodes-base/nodes/Code/Sandbox.ts
index b3e3057d23..0cfa4fb534 100644
--- a/packages/nodes-base/nodes/Code/Sandbox.ts
+++ b/packages/nodes-base/nodes/Code/Sandbox.ts
@@ -185,13 +185,37 @@ export abstract class Sandbox extends EventEmitter {
}
private validateTopLevelKeys(item: INodeExecutionData, itemIndex: number) {
- Object.keys(item).forEach((key) => {
- if (REQUIRED_N8N_ITEM_KEYS.has(key)) return;
+ let foundReservedKey: string | null = null;
+ const unknownKeys: string[] = [];
+
+ for (const key in item) {
+ if (!Object.prototype.hasOwnProperty.call(item, key)) continue;
+
+ if (REQUIRED_N8N_ITEM_KEYS.has(key)) {
+ foundReservedKey ??= key;
+ } else {
+ unknownKeys.push(key);
+ }
+ }
+
+ if (unknownKeys.length > 0) {
+ if (foundReservedKey) throw new ReservedKeyFoundError(foundReservedKey, itemIndex);
+
throw new ValidationError({
- message: `Unknown top-level item key: ${key}`,
+ message: `Unknown top-level item key: ${unknownKeys[0]}`,
description: 'Access the properties of an item under `.json`, e.g. `item.json`',
itemIndex,
});
+ }
+ }
+}
+
+class ReservedKeyFoundError extends ValidationError {
+ constructor(reservedKey: string, itemIndex: number) {
+ super({
+ message: 'Invalid output format',
+ description: `An output item contains the reserved key ${reservedKey}. To get around this, please wrap each item in an object, under a key called json. Example`,
+ itemIndex,
});
}
}