pydantic/monty · error · TypeError
Cannot convert ${constructorName(value)} instance to a Monty
Error message
Cannot convert ${constructorName(value)} instance to a Monty value — wrap it in ClassInstance(...) What it means
Class instances of arbitrary host constructors cannot be serialized across the sandbox boundary; Monty only carries values it understands or explicitly registered class instances. The prepare walk throws this TypeError naming the constructor when it meets a non-plain object it cannot convert. Wrap the instance with ClassInstance(...) (with a ClassType) to expose it to the sandbox.
Source
Thrown at crates/monty-js/ts/classInstance.ts:553
if (marker === 'ClassInstance') {
// Identity-bearing markers are produced internally by this walk, never
// held by host code (`restore` maps them to the original object or a
// MontyClassProxy). One arriving here is forged — e.g. embedded
// in attacker-controlled JSON to impersonate a registered instance.
throw new TypeError('raw ClassInstance markers are not accepted — wrap the object in ClassInstance(...)')
}
if (marker === 'Type' && (value as { classType?: unknown }).classType !== undefined) {
// Same reasoning for a host-class marker; builtin `Type` markers
// (`{ value: 'int' }`) carry no identity and pass through.
throw new TypeError('raw Type markers are not accepted — pass the class through ClassType(...)')
}
if (marker !== undefined) {
return value
}
if (isPlainObject(value)) {
return walkPlainObject(value as Record<string, unknown>, walk)
}
throw new TypeError(
`Cannot convert ${constructorName(value)} instance to a Monty value — wrap it in ClassInstance(...)`,
)
}
/**
* Inbound walk over a sandbox value reaching the host: maps `ClassInstance`
* markers to the original wrapped object when the id is in `store` (identity
* preserved), else to a [`MontyClassProxy`] proxy with recursively
* restored attrs; maps a host-class `Type` marker to the registered class
* object the same way (an unregistered class stays a marker); recurses into
* containers. Wire values are already depth-bounded by the native layer, so
* no guard is needed here.
*/
export function restore(value: unknown, store: InstanceStore): unknown {
if (typeof value !== 'object' || value === null) {
return value
}
const walk = (item: unknown) => restore(item, store)View on GitHub (pinned to adc986b362)
Solutions
- Wrap the instance: ClassInstance(instance, ClassType(SomeClass))
- Convert the value to a plain object/array/primitive before passing it
- Register the class once via the class-registration API and consistently wrap every instance you send
Example fix
// before
session.feedRun('describe(user)', { inputs: { user: new User('ada') } });
// after
session.feedRun('describe(user)', { inputs: { user: ClassInstance(new User('ada'), ClassType(User)) } }); Defensive patterns
Strategy: validation
Validate before calling
function isMontyConvertible(v, seen = new Set()) {
if (v === null || ['string', 'number', 'boolean'].includes(typeof v)) return true;
if (typeof v !== 'object') return false;
if (seen.has(v)) return true;
seen.add(v);
const proto = Object.getPrototypeOf(v);
if (proto !== Object.prototype && proto !== Array.prototype) return false;
return Object.values(v).every((x) => isMontyConvertible(x, seen));
}
if (!isMontyConvertible(inputs)) console.error('non-plain object in inputs; wrap with ClassInstance or convert'); Type guard
function isPlainObjectOrArray(v) {
if (v === null || typeof v !== 'object') return false;
const p = Object.getPrototypeOf(v);
return p === Object.prototype || p === Array.prototype;
} Try / catch
try {
await session.feedRun(code, { inputs });
} catch (e) {
const m = e instanceof TypeError ? /Cannot convert (\S+) instance/.exec(e.message) : null;
if (m) throw new Error(`${m[1]} must be wrapped in ClassInstance(...) or converted to a plain value`);
throw e;
} Prevention
- Only send plain objects, arrays and primitives unless the value is deliberately wrapped in ClassInstance(...)
- Convert Date/Map/Set to plain representations at the boundary
- Register classes once and always wrap instances through a shared helper
When it happens
Trigger: Passing a Map, Set, Date, RegExp, class instance, or other exotic object (constructorName(value) reveals which) as a feedRun input, external-function return, or nested attribute without wrapping it.
Common situations: Returning ORM model instances or framework objects from callbacks; passing Date/Map values that were assumed JSON-convertible; forgetting to wrap an object registered elsewhere in the same call.
Understand the failure class
Background: "is not a compatible type" / "cannot merge" errors: when a value's type doesn't match what the library requires — this error's family across 65 libraries.
Related errors
- classType does not match the instance's class
- ClassType expects a class (constructor function)
- raw ClassInstance markers are not accepted — wrap the object
- ClassInstance marker instanceId must be a uuid string
- Unknown Monty marker type: ${String(object[TYPE_MARKER])}
AI-assisted analysis of pydantic/monty@adc986b362 (2026-09-13).
Data as JSON: /api/errors/8f1c3e8be3ccfcb1.
Report an issue: GitHub.