Skip to content

Expressions: a class instance in workflow data loses its overridden toString (MongoDB _id gives [object Object]) #37420

Description

@aniketwaghh

Bug Description

A value in workflow data whose meaning lives on its prototype loses it crossing into the expression runtime, so an expression that relied on toString gets [object Object] and nothing is raised.

The case that surfaced it is a MongoDB _id: {{ $json._id.toString() }} returned the hex before the isolate expression runtime and returns [object Object] after. @chung1912 reported it from a live 2.36.8 instance in #36473 (comment). It is a different root cause from the key-shadowing bug #36473 tracks, and fixing that one will not fix this, so it needs its own issue.

Only Object.keys crosses the boundary. In packages/@n8n/expression-runtime/src/bridge/isolated-vm-bridge.ts a non-Date object is marshaled as:

// Dates have no enumerable own keys; pass through instead of
// marshaling as an empty object.
if (value instanceof Date) {
    return value;
}

if (value !== null && typeof value === 'object') {
    return { __isObject: true, __keys: Object.keys(value) };
}

For a driver-returned ObjectId, Object.keys() is ["buffer"], so the prototype — and the toString that holds the hex — is dropped and Object.prototype.toString answers instead. Date was given a pass-through for exactly this reason, and src/__tests__/integration.test.ts already has a "Date marshaling from workflow data" block from an earlier round of the same problem. This is that problem for the classes the runtime cannot know by name.

Both marshaling sites are affected: getValueAtPath and getArrayElement.

Not limited to ObjectIdDecimal128 loses toString the same way, so {{ $json.price.toString() }} gives [object Object] instead of 12.345.

For the "is this even supported data" question: GenericValue in packages/workflow/src/Interfaces.ts is string | object | number | boolean | undefined | null, so a class instance in item.json is within the declared type.

To Reproduce

Round-tripped through a real MongoDB 7 and the real driver (mongodb 6.11.0, the version nodes-base resolves), so this is a driver-returned ObjectId rather than a hand-made stand-in:

const col = client.db('e2e').collection('docs');
await col.insertOne({ name: 'invoice', amount: 42 });
const doc = await col.findOne({ name: 'invoice' });   // what find() hands downstream

// doc._id is an ObjectId; Object.keys(doc._id) === ["buffer"]
evaluator.evaluate('{{ $json._id.toString() }}', { $json: doc }, caller);

On master at 021cdf26:

driver returned _id        -> ObjectId 6a9568ffce3ca127e583b1bb
{{ $json._id.toString() }} -> "[object Object]"      ← expected the hex
{{ "id=" + $json._id }}    -> "id=[object Object]"
$json.row._id.toString()   -> "[object Object]"       (nested)
$json.ids[0].toString()    -> "[object Object]"       (inside an array)
{{ $json.price.toString() }} -> "[object Object]"     (Decimal128, expected "12.345")

{{ $json.name }}           -> "invoice"               (sibling fields fine)
{{ $json.d.toISOString() }} -> "2026-06-30T20:34:04.498Z"   (Date, special-cased, fine)

Expected behavior

{{ $json._id.toString() }} gives the hex, as it did before the isolate runtime, and "id=" + $json._id concatenates it.

Only values whose toString is genuinely overridden should change. A plain object must keep returning [object Object], and an array must keep returning its comma-joined form.

Debug Info

The live-instance report is @chung1912's on 2.36.8, linked above. The reproduction here is against master at 021cdf26 (repo version 2.37.0) driving the real IsolatedVmBridge with a document fetched from a real MongoDB, so it is not from an n8n UI session and the usual Debug Info panel does not apply.

Operating System

macOS 26.6.2 (MongoDB 7 in Docker)

n8n Version

2.37.0 (master at 021cdf26); reported live on 2.36.8, and @chung1912 notes it worked before 2.34.x

Node.js Version

24.19.0

Database

n/a for n8n itself; MongoDB 7 as the data source

Relation to #36473

Two opposite defects, easy to conflate:

A fix for one does not fix the other — verified, not assumed: with the fix for this issue applied and the key-shadowing fix absent, a data key named toString still resolves to the prototype method.

#37422 fixes this one and keeps the two independent.

Metadata

Metadata

Assignees

No one assigned

    Labels

    status:in-linearIssue or PR is now in Linearstatus:team-assignedA team has been assigned the issue or PRteam:catsIssue is with the Cats team

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions